コード例 #1
0
class BotTrapCheck(GenericRegistrationInspector):
    _domain = 'acct_mgr'
    _description = cleandoc_("""
    A collection of simple bot checks.

    ''This check is bypassed for requests by an authenticated user.''
    """)

    reg_basic_question = Option(
        'account-manager', 'register_basic_question', '',
        doc="A question to ask instead of the standard prompt, to which "
            "the value of register_basic_token is the answer. Setting to "
            "empty string (default value) keeps the standard prompt.")
    reg_basic_token = Option(
        'account-manager', 'register_basic_token', '',
        doc="A string required as input to pass verification.")

    def render_registration_fields(self, req, data):
        """Add a hidden text input field to the registration form, and
        a visible one with mandatory input as well, if token is configured.
        """
        if self.reg_basic_token:
            # Preserve last input for editing on failure instead of typing
            # everything again.
            old_value = req.args.get('basic_token', '')

            if self.reg_basic_question:
                # TRANSLATOR: Question-style hint for visible bot trap
                # registration input field.
                hint = tag.p(_("Please answer above: %(question)s",
                               question=self.reg_basic_question),
                             class_='hint')
            else:
                # TRANSLATOR: Verbatim token hint for visible bot trap
                # registration input field.
                hint = tag.p(tag_(
                    "Please type [%(token)s] as verification token, "
                    "exactly replicating everything within the braces.",
                    token=tag.b(self.reg_basic_token)), class_='hint')
            insert = tag(
                tag.label(_("Parole:"),
                          tag.input(type='text', name='basic_token', size=20,
                                    class_='textwidget', value=old_value)),
                hint)
        else:
            insert = None
        # TRANSLATOR: Registration form hint for hidden bot trap input field.
        insert = tag(insert,
                     tag.input(type='hidden', name='sentinel',
                               title=_("Better do not fill this field.")))
        return insert, data

    def validate_registration(self, req):
        if req.authname and req.authname != 'anonymous':
            return
        # Input must be an exact replication of the required token.
        basic_token = req.args.get('basic_token', '')
        # Unlike the former, the hidden bot-trap input field must stay empty.
        keep_empty = req.args.get('sentinel', '')
        if keep_empty or self.reg_basic_token and \
                        self.reg_basic_token != basic_token:
            raise RegistrationError(N_("Are you human? If so, try harder!"))
コード例 #2
0
ファイル: svn_authz.py プロジェクト: zxfly/trac
class AuthzSourcePolicy(Component):
    """Permission policy for `source:` and `changeset:` resources using a
    Subversion authz file.

    `FILE_VIEW` and `BROWSER_VIEW` permissions are granted as specified in the
    authz file.

    `CHANGESET_VIEW` permission is granted for changesets where `FILE_VIEW` is
    granted on at least one modified file, as well as for empty changesets.
    """

    implements(IPermissionPolicy)

    authz_file = PathOption('svn',
                            'authz_file',
                            '',
                            """The path to the Subversion
        [%(svnbook)s authorization (authz) file].
        To enable authz permission checking, the `AuthzSourcePolicy`
        permission policy must be added to `[trac] permission_policies`.
        Non-absolute paths are relative to the Environment `conf`
        directory.
        """,
                            doc_args={
                                'svnbook':
                                'http://svnbook.red-bean.com/en/1.7/'
                                'svn.serverconfig.pathbasedauthz.html'
                            })

    authz_module_name = Option(
        'svn', 'authz_module_name', '',
        """The module prefix used in the `authz_file` for the default
        repository. If left empty, the global section is used.
        """)

    _handled_perms = frozenset([(None, 'BROWSER_VIEW'),
                                (None, 'CHANGESET_VIEW'), (None, 'FILE_VIEW'),
                                (None, 'LOG_VIEW'), ('source', 'BROWSER_VIEW'),
                                ('source', 'FILE_VIEW'),
                                ('source', 'LOG_VIEW'),
                                ('changeset', 'CHANGESET_VIEW')])

    def __init__(self):
        self._mtime = 0
        self._authz = {}
        self._users = set()

    # IPermissionPolicy methods

    def check_permission(self, action, username, resource, perm):
        realm = resource.realm if resource else None
        if (realm, action) in self._handled_perms:
            authz, users = self._get_authz_info()
            if authz is None:
                return False

            if username == 'anonymous':
                usernames = '$anonymous', '*'
            else:
                usernames = username, '$authenticated', '*'
            if resource is None:
                return True if users & set(usernames) else None

            rm = RepositoryManager(self.env)
            try:
                repos = rm.get_repository(resource.parent.id)
            except TracError:
                return True  # Allow error to be displayed in the repo index
            if repos is None:
                return True
            modules = [resource.parent.id or self.authz_module_name]
            if modules[0]:
                modules.append('')

            def check_path_0(spath):
                sections = [
                    authz.get(module, {}).get(spath) for module in modules
                ]
                sections = [section for section in sections if section]
                denied = False
                for user in usernames:
                    for section in sections:
                        if user in section:
                            if section[user]:
                                return True
                            denied = True
                            # Don't check section without module name
                            # because the section with module name defines
                            # the user's permissions.
                            break
                if denied:  # All users has no readable permission.
                    return False

            def check_path(path):
                path = '/' + pathjoin(repos.scope, path)
                if path != '/':
                    path += '/'

                # Allow access to parent directories of allowed resources
                for spath in set(
                        sum((list(authz.get(module, {}))
                             for module in modules), [])):
                    if spath.startswith(path):
                        result = check_path_0(spath)
                        if result is True:
                            return True

                # Walk from resource up parent directories
                for spath in parent_iter(path):
                    result = check_path_0(spath)
                    if result is not None:
                        return result

            if realm == 'source':
                return check_path(resource.id)

            elif realm == 'changeset':
                changes = list(repos.get_changeset(resource.id).get_changes())
                if not changes or any(
                        check_path(change[0]) for change in changes):
                    return True

    def _get_authz_info(self):
        if not self.authz_file:
            self.log.error("The [svn] authz_file configuration option in "
                           "trac.ini is empty or not defined")
            raise ConfigurationError()
        try:
            mtime = os.path.getmtime(self.authz_file)
        except OSError as e:
            self.log.error(
                "Error accessing svn authz permission policy "
                "file: %s", exception_to_unicode(e))
            raise ConfigurationError()
        if mtime != self._mtime:
            self._mtime = mtime
            rm = RepositoryManager(self.env)
            modules = set(repos.reponame
                          for repos in rm.get_real_repositories())
            if '' in modules and self.authz_module_name:
                modules.add(self.authz_module_name)
            modules.add('')
            self.log.info("Parsing authz file: %s", self.authz_file)
            try:
                self._authz = parse(self.authz_file, modules)
            except ParsingError as e:
                self.log.error(
                    "Error parsing svn authz permission policy "
                    "file: %s", exception_to_unicode(e))
                raise ConfigurationError()
            else:
                self._users = {
                    user
                    for paths in self._authz.itervalues()
                    for path in paths.itervalues()
                    for user, result in path.iteritems() if result
                }
        return self._authz, self._users
コード例 #3
0
def get_estimation_suffix():
    return Option('estimation-tools',
                  'estimation_suffix',
                  'h',
                  doc="""Suffix used for estimations. Defaults to 'h'""")
コード例 #4
0
class AuthCaptcha(Component):

    ### class data
    implements(IRequestFilter, ITemplateStreamFilter, ITemplateProvider,
               IAuthenticator, IEnvironmentSetupParticipant,
               IRequireComponents, INavigationContributor)

    dict_file = Option(
        'captchaauth',
        'dictionary_file',
        default=
        "http://java.sun.com/docs/books/tutorial/collections/interfaces/examples/dictionary.txt"
    )
    captcha_type = Option('captchaauth', 'type', default="png")
    realms = ListOption('captchaauth', 'realms', default="wiki, newticket")
    permissions = {
        'wiki': ['WIKI_CREATE', 'WIKI_MODIFY'],
        'newticket': ['TICKET_CREATE']
    }

    xpath = {'ticket.html': "//div[@class='buttons']"}
    delete = {'ticket.html': "//div[@class='field']"}

    ### IRequestFilter methods

    def pre_process_request(self, req, handler):
        """Called after initial handler selection, and can be used to change
        the selected handler or redirect request.
        
        Always returns the request handler, even if unchanged.
        """

        if req.method == 'GET':
            if req.path_info.strip('/') in ['register', 'login'
                                            ] and req.authname != 'anonymous':
                login_module = LoginModule(self.env)
                login_module._do_logout(req)
                req.redirect(req.href(req.path_info))

        if req.method == 'POST':

            realm = self.realm(req)

            # set the session data for name and email if CAPTCHA-authenticated
            if 'captchaauth' in req.args:
                name, email = self.identify(req)
                for field in 'name', 'email':
                    value = locals()[field]
                    if value:
                        req.session[field] = value
                req.session.save()
                if req.authname != 'anonymous' and realm == 'newticket':
                    req.args['author'] = name
                    if email:
                        req.args['author'] += ' <%s>' % email

            # redirect anonymous user posts that are not CAPTCHA-identified
            if req.authname == 'anonymous' and realm in self.realms:

                if 'captchaauth' in req.args and 'captchaid' in req.args:
                    # add warnings from CAPTCHA authentication
                    captcha = self.captcha(req)
                    if req.args['captchaauth'] != captcha:
                        add_warning(
                            req, "You typed the wrong word. Please try again.")
                        try:
                            # delete used CAPTCHA
                            execute_non_query(
                                self.env, "DELETE FROM captcha WHERE id=%s",
                                req.args['captchaid'])
                        except:
                            pass

                    name, email = self.identify(req)
                    if not name:
                        add_warning(req, 'Please provide your name')
                    if AccountManager and name in AccountManager(
                            self.env).get_users():
                        add_warning(
                            req,
                            '%s is already taken as by a registered user.  Please login or use a different name'
                            % name)

                # redirect to previous location
                location = req.get_header('referer')
                if location:
                    location, query = urllib.splitquery(location)

                    if realm == 'newticket':
                        args = [(key.split('field_', 1)[-1], value)
                                for key, value in req.args.items()
                                if key.startswith('field_')]
                        location += '?%s' % urllib.urlencode(args)
                else:
                    location = req.href()
                req.redirect(location)

        return handler

    # for ClearSilver templates
    def post_process_request(self, req, template, content_type):
        """Do any post-processing the request might need; typically adding
        values to req.hdf, or changing template or mime type.
        
        Always returns a tuple of (template, content_type), even if
        unchanged.

        Note that `template`, `content_type` will be `None` if:
         - called when processing an error page
         - the default request handler did not return any result

        (for 0.10 compatibility; only used together with ClearSilver templates)
        """
        return (template, content_type)

    # for Genshi templates
    def post_process_request(self, req, template, data, content_type):
        """Do any post-processing the request might need; typically adding
        values to the template `data` dictionary, or changing template or
        mime type.
        
        `data` may be update in place.

        Always returns a tuple of (template, data, content_type), even if
        unchanged.

        Note that `template`, `data`, `content_type` will be `None` if:
         - called when processing an error page
         - the default request handler did not return any result

        (Since 0.11)
        """
        return (template, data, content_type)

    ### ITemplateStreamFilter method

    def filter_stream(self, req, method, filename, stream, data):
        """Return a filtered Genshi event stream, or the original unfiltered
        stream if no match.

        `req` is the current request object, `method` is the Genshi render
        method (xml, xhtml or text), `filename` is the filename of the template
        to be rendered, `stream` is the event stream and `data` is the data for
        the current template.

        See the Genshi documentation for more information.
        """

        # only show CAPTCHAs for anonymous users
        if req.authname != 'anonymous':
            return stream

        # only put CAPTCHAs in the realms specified
        realm = self.realm(req)
        if realm not in self.realms:
            return stream

        # add the CAPTCHA to the stream
        if filename in self.xpath:

            # store CAPTCHA in DB and session
            word = random_word(self.dict_file)
            insert_update(self.env, 'captcha', 'id', req.session.sid,
                          dict(word=word))
            req.session['captcha'] = word
            req.session.save()

            # render the template
            chrome = Chrome(self.env)
            template = chrome.load_template('captcha.html')
            _data = {}

            # CAPTCHA type
            if self.captcha_type == 'png':
                captcha = tag.img(None, src=req.href('captcha.png'))
            else:
                captcha = Markup(skimpyAPI.Pre(word).data())

            _data['captcha'] = captcha
            _data['email'] = req.session.get('email', '')
            _data['name'] = req.session.get('name', '')
            _data['captchaid'] = req.session.sid
            xpath = self.xpath[filename]
            stream |= Transformer(xpath).before(template.generate(**_data))
            if filename in self.delete:
                stream |= Transformer(self.delete[filename]).remove()

        return stream

    ### methods for ITemplateProvider
    """Extension point interface for components that provide their own
    ClearSilver templates and accompanying static resources.
    """

    def get_htdocs_dirs(self):
        """Return a list of directories with static resources (such as style
        sheets, images, etc.)

        Each item in the list must be a `(prefix, abspath)` tuple. The
        `prefix` part defines the path in the URL that requests to these
        resources are prefixed with.
        
        The `abspath` is the absolute path to the directory containing the
        resources on the local file system.
        """
        return []

    def get_templates_dirs(self):
        """Return a list of directories containing the provided template
        files.
        """
        return [resource_filename(__name__, 'templates')]

    ### method for IAuthenticator
    """Extension point interface for components that can provide the name
    of the remote user."""

    def authenticate(self, req):
        """Return the name of the remote user, or `None` if the identity of the
        user is unknown."""

        # check for an authenticated user
        login_module = LoginModule(self.env)
        remote_user = login_module.authenticate(req)
        if remote_user:
            return remote_user

        # authenticate via a CAPTCHA
        if 'captchaauth' in req.args and 'captchaid' in req.args:

            # ensure CAPTCHA identification
            captcha = self.captcha(req)
            if captcha != req.args['captchaauth']:
                return

            # ensure sane identity
            name, email = self.identify(req)
            if name is None:
                return
            if AccountManager and name in AccountManager(self.env).get_users():
                return

            # delete used CAPTCHA on success
            try:
                execute_non_query(self.env, "DELETE FROM captcha WHERE id=%s",
                                  req.args['captchaid'])
            except:
                pass

            # log the user in
            req.environ['REMOTE_USER'] = name
            login_module._do_login(req)

    ### methods for INavigationContributor
    """Extension point interface for components that contribute items to the
    navigation.
    """

    def get_active_navigation_item(self, req):
        """This method is only called for the `IRequestHandler` processing the
        request.
        
        It should return the name of the navigation item that should be
        highlighted as active/current.
        """
        return None

    def get_navigation_items(self, req):
        """Should return an iterable object over the list of navigation items to
        add, each being a tuple in the form (category, name, text).
        """
        if req.authname != 'anonymous' and 'captcha' in req.session:
            return [('metanav', '_login', tag.a("Login",
                                                href=req.href.login())),
                    ('metanav', '_register',
                     tag.a("Register", href=req.href.register()))]
        return []

    ### methods for IEnvironmentSetupParticipant
    """Extension point interface for components that need to participate in the
    creation and upgrading of Trac environments, for example to create
    additional database tables."""

    def environment_created(self):
        """Called when a new Trac environment is created."""
        if self.environment_needs_upgrade(None):
            self.upgrade_environment(None)

    def environment_needs_upgrade(self, db):
        """Called when Trac checks whether the environment needs to be upgraded.
        
        Should return `True` if this participant needs an upgrade to be
        performed, `False` otherwise.
        """
        try:
            get_table(self.env, 'captcha')
        except:
            return True
        return False

    def upgrade_environment(self, db):
        """Actually perform an environment upgrade.
        
        Implementations of this method should not commit any database
        transactions. This is done implicitly after all participants have
        performed the upgrades they need without an error being raised.
        """

        # table of CAPTCHAs
        captcha_table = Table('captcha', key='key')[Column('id'),
                                                    Column('word')]
        create_table(self.env, captcha_table)

    ### method for IRequireComponents

    def requires(self):
        """list of component classes that this component depends on"""
        return [ImageCaptcha]

    ### internal methods

    def identify(self, req):
        """
        identify the user, ensuring uniqueness (TODO);
        returns a tuple of (name, email) or success or None
        """
        name = req.args.get('name', None).strip()
        email = req.args.get('email', None).strip()
        return name, email

    def realm(self, req):
        """
        returns the realm according to the request
        """
        path = req.path_info.strip('/').split('/')
        if not path:
            return
        # TODO: default handler ('/')
        return path[0]

    def captcha(self, req):
        return get_scalar(self.env, "SELECT word FROM captcha WHERE id=%s", 0,
                          req.args['captchaid'])
コード例 #5
0
ファイル: web_ui.py プロジェクト: rwbaumg/tractagsplugin
class TagInputAutoComplete(TagTemplateProvider):
    """[opt] Provides auto-complete functionality for tag input fields.

    This module is based on KeywordSuggestModule from KeywordSuggestPlugin
    0.5dev.
    """

    implements(IRequestFilter, ITemplateStreamFilter)

    field_opt = Option(
        'tags', 'complete_field', 'keywords',
        "Ticket field to which a drop-down tag list should be attached.")

    help_opt = Option(
        'tags', 'ticket_help', None,
        "If specified, 'keywords' label on ticket view will be turned into a "
        "link to this URL.")

    helpnewwindow_opt = BoolOption(
        'tags', 'ticket_help_newwindow', False,
        "If true and keywords_help specified, wiki page will open in a new "
        "window. Default is false.")

    # Needs to be reimplemented, refs th:#8141.
    #mustmatch = BoolOption('tags', 'complete_mustmatch', False,
    #    "If true, input fields accept values from the word list only.")

    matchcontains_opt = BoolOption(
        'tags', 'complete_matchcontains', True,
        "Include partial matches in suggestion list. Default is true.")

    separator_opt = Option(
        'tags', 'separator', ' ',
        "Character(s) to use as separators between tags. Default is a "
        "single whitespace.")

    sticky_tags_opt = ListOption(
        'tags',
        'complete_sticky_tags',
        '',
        ',',
        doc="A list of comma separated values available for input.")

    def __init__(self):
        self.tags_enabled = self.env.is_enabled(TagSystem)

    @property
    def separator(self):
        return self.separator_opt.strip('\'') or ' '

    # IRequestFilter methods

    def pre_process_request(self, req, handler):
        return handler

    def post_process_request(self, req, template, data, content_type):
        if template is not None and \
                (req.path_info.startswith('/ticket/') or
                 req.path_info.startswith('/newticket') or
                 (self.tags_enabled and req.path_info.startswith('/wiki/'))):
            # In Trac 1.0 and later, jQuery-UI is included from the core.
            if trac_version >= '1.0':
                Chrome(self.env).add_jquery_ui(req)
            else:
                add_script(req, 'tags/js/jquery-ui-1.8.16.custom.min.js')
                add_stylesheet(req, 'tags/css/jquery-ui-1.8.16.custom.css')
        return template, data, content_type

    # ITemplateStreamFilter method
    def filter_stream(self, req, method, filename, stream, data):

        if not (filename == 'ticket.html' or
                (self.tags_enabled and filename == 'wiki_edit.html')):
            return stream

        keywords = self._get_keywords_string(req)
        if not keywords:
            self.log.debug(
                "No keywords found. TagInputAutoComplete is disabled.")
            return stream

        matchfromstart = '"^" +'
        if self.matchcontains_opt:
            matchfromstart = ''

        js = """
            jQuery(document).ready(function($) {
                var keywords = [ %(keywords)s ]
                var sep = '%(separator)s'.trim() + ' '
                function split( val ) {
                    return val.split( /%(separator)s\s*|\s+/ );
                }
                function extractLast( term ) {
                    return split( term ).pop();
                }
                $('%(field)s')
                    // don't navigate away from field on tab when selecting
                    // an item
                    .bind( "keydown", function( event ) {
                        if ( event.keyCode === $.ui.keyCode.TAB &&
                             $( this ).data( "autocomplete" ).menu.active ) {
                            event.preventDefault();
                        }
                    })
                    .autocomplete({
                        delay: 0,
                        minLength: 0,
                        source: function( request, response ) {
                            // delegate back to autocomplete, but extract
                            // the last term
                            response( $.ui.autocomplete.filter(
                                keywords, extractLast( request.term ) ) );
                        },
                        focus: function() {
                            // prevent value inserted on focus
                            return false;
                        },
                        select: function( event, ui ) {
                            var terms = split( this.value );
                            // remove the current input
                            terms.pop();
                            // add the selected item
                            terms.push( ui.item.value );
                            // add placeholder to get the comma-and-space at
                            // the end
                            terms.push( "" );
                            this.value = terms.join( sep );
                            return false;
                        }
                    });
            });"""

        # Inject transient part of JavaScript into ticket.html template.
        if req.path_info.startswith('/ticket/') or \
           req.path_info.startswith('/newticket'):
            js_ticket = js % {
                'field': '#field-' + self.field_opt,
                'keywords': keywords,
                'matchfromstart': matchfromstart,
                'separator': self.separator
            }
            stream = stream | Transformer('.//head')\
                              .append(builder.script(Markup(js_ticket),
                                      type='text/javascript'))

            # Turn keywords field label into link to an arbitrary resource.
            if self.help_opt:
                link = self._get_help_link(req)
                if self.helpnewwindow_opt:
                    link = builder.a(href=link, target='blank')
                else:
                    link = builder.a(href=link)
                xpath = '//label[@for="field-keywords"]/text()'
                stream = stream | Transformer(xpath).wrap(link)

        # Inject transient part of JavaScript into wiki.html template.
        elif self.tags_enabled and req.path_info.startswith('/wiki/'):
            js_wiki = js % {
                'field': '#tags',
                'keywords': keywords,
                'matchfromstart': matchfromstart,
                'separator': self.separator
            }
            stream = stream | Transformer('.//head')\
                              .append(builder.script(Markup(js_wiki),
                                                     type='text/javascript'))
        return stream

    # Private methods

    def _get_keywords_string(self, req):
        keywords = set(self.sticky_tags_opt)  # prevent duplicates
        if self.tags_enabled:
            # Use TagsPlugin >= 0.7 performance-enhanced API.
            tags = TagSystem(self.env).get_all_tags(req)
            keywords.update(tags.keys())

        if keywords:
            keywords = sorted(keywords)
            keywords = ','.join(
                ("'%s'" % javascript_quote(_keyword) for _keyword in keywords))
        else:
            keywords = ''

        return keywords

    def _get_help_link(self, req):
        link = resource_id = None
        if self.help_opt.startswith('/'):
            # Assume valid URL to arbitrary resource inside
            #   of the current Trac environment.
            link = req.href(self.help_opt)
        if not link and ':' in self.help_opt:
            realm, resource_id = self.help_opt.split(':', 1)
            # Validate realm-like prefix against resource realm list,
            #   but exclude 'wiki' to allow deferred page creation.
            rsys = ResourceSystem(self.env)
            if realm in set(rsys.get_known_realms()) - set('wiki'):
                mgr = rsys.get_resource_manager(realm)
                # Handle optional IResourceManager method gracefully.
                try:
                    if mgr.resource_exists(Resource(realm, resource_id)):
                        link = mgr.get_resource_url(resource_id, req.href)
                except AttributeError:
                    # Assume generic resource URL build rule.
                    link = req.href(realm, resource_id)
        if not link:
            if not resource_id:
                # Assume wiki page name for backwards-compatibility.
                resource_id = self.help_opt
            # Preserve anchor without 'path_safe' arg (since Trac 0.12.2dev).
            if '#' in resource_id:
                path, anchor = resource_id.split('#', 1)
            else:
                anchor = None
                path = resource_id
            if hasattr(unicode_quote_plus, "safe"):
                # Use method for query string quoting (since Trac 0.13dev).
                anchor = unicode_quote_plus(anchor, safe="?!~*'()")
            else:
                anchor = unicode_quote_plus(anchor)
            link = '#'.join([req.href.wiki(path), anchor])
        return link
コード例 #6
0
class TracJSGanttSupport(Component):
    implements(IRequestFilter, ITemplateProvider)

    Option('trac-jsgantt', 'option.format', 'day',
           """Initial format of Gantt chart""")
    Option('trac-jsgantt', 'option.formats', 'day|week|month|quarter',
           """Formats to show for Gantt chart""")
    IntOption('trac-jsgantt', 'option.sample', 0, """Show sample Gantt""")
    IntOption('trac-jsgantt', 'option.res', 1, """Show resource column""")
    IntOption('trac-jsgantt', 'option.dur', 1, """Show duration column""")
    IntOption('trac-jsgantt', 'option.comp', 1,
              """Show percent complete column""")
    Option('trac-jsgantt', 'option.caption', 'Resource',
           """Caption to follow task in Gantt""")
    IntOption('trac-jsgantt', 'option.startDate', 1,
              """Show start date column""")
    IntOption('trac-jsgantt', 'option.endDate', 1,
              """Show finish date column""")
    Option('trac-jsgantt', 'option.dateDisplay', 'mm/dd/yyyy',
           """Format to display dates""")
    IntOption('trac-jsgantt', 'option.openLevel', 999,
              """How many levels of task hierarchy to show open""")
    IntOption('trac-jsgantt', 'option.expandClosedTickets', 1,
              """Show children of closed tasks in the task hierarchy""")
    Option('trac-jsgantt', 'option.colorBy', 'priority',
           """Field to use to color tasks""")
    IntOption('trac-jsgantt', 'option.lwidth', None,
              """Width (in pixels) of left table""")
    IntOption('trac-jsgantt', 'option.showdep', 1,
              """Show dependencies in Gantt""")
    IntOption('trac-jsgantt', 'option.userMap', 1,
              """Map user IDs to user names""")
    IntOption('trac-jsgantt', 'option.omitMilestones', 0,
              """Omit milestones""")
    Option('trac-jsgantt', 'option.schedule', 'alap',
           """Schedule algorithm: alap or asap""")
    IntOption('trac-jsgantt', 'option.doResourceLeveling', 0,
              """Resource level (1) or not (0)""")
    # This seems to be the first floating point option.
    Option('trac-jsgantt', 'option.hoursPerDay', '8.0',
           """Hours worked per day""")
    Option(
        'trac-jsgantt', 'option.display', None,
        """Display filter for tickets in the form 'field1:value1|field2:value2' or 'field:value1|value2'; displays tickets where field1==value1, etc."""
    )
    Option(
        'trac-jsgantt', 'option.order', 'wbs',
        """Fields to sort tasks by before display.  May include tickets fields (including custom fields) or 'wbs'."""
    )
    Option('trac-jsgantt', 'option.scrollTo', None,
           """Date to scroll chart to (yyyy-mm--dd or 'today')""")

    Option(
        'trac-jsGantt', 'option.linkStyle', 'standard',
        """Style for ticket links; jsgantt (new window) or standard browser behavior like ticket links."""
    )

    # ITemplateProvider methods
    def get_htdocs_dirs(self):
        return [('tracjsgantt', resource_filename(__name__, 'htdocs'))]

    def get_templates_dirs(self):
        return []

    # IRequestFilter methods
    def pre_process_request(self, req, handler):
        # I think we should look for a TracJSGantt on the page and set
        # a flag for the post_process_request handler if found
        return handler

    def post_process_request(self, req, template, data, content_type):
        add_script(req, 'tracjsgantt/jsgantt.js')
        add_stylesheet(req, 'tracjsgantt/jsgantt.css')
        add_stylesheet(req, 'tracjsgantt/tracjsgantt.css')
        return template, data, content_type
コード例 #7
0
class CodeReviewerModule(Component):
    """Base component for reviewing changesets."""

    implements(ITemplateProvider, IRequestFilter)

    # config options
    statuses = ListOption('codereviewer',
                          'status_choices',
                          default=CodeReview.STATUSES,
                          doc="Review status choices.")

    passed = ListOption('codereviewer',
                        'passed',
                        default=[],
                        doc="Ticket field changes on a PASSED submit.")

    failed = ListOption('codereviewer',
                        'failed',
                        default=[],
                        doc="Ticket field changes on a FAILED submit.")

    completeness = ListOption(
        'codereviewer',
        'completeness',
        default=[],
        doc="Ticket field values enabling ticket completeness.")

    command = Option('codereviewer',
                     'command',
                     default='',
                     doc="Command to execute upon ticket completeness.")

    # ITemplateProvider methods

    def get_htdocs_dirs(self):
        from pkg_resources import resource_filename
        return [('coderev', resource_filename(__name__, 'htdocs'))]

    def get_templates_dirs(self):
        return []

    # IRequestFilter methods

    def pre_process_request(self, req, handler):
        return handler

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

        if req.path_info.startswith('/changeset') and \
                data.get('changeset') is not False and \
                'CODEREVIEWER_MODIFY' in req.perm:
            changeset = data['changeset']
            repos = changeset.repos
            reponame, rev = repos.reponame, repos.db_rev(changeset.rev)
            review = CodeReview(self.env, reponame, rev)
            tickets = req.args.getlist('tickets')
            if req.method == 'POST':
                status_changed = \
                    review.encode(req.args['status']) != review.status
                if review.save(req.authname, req.args['status'],
                               req.args['summary']):
                    self._update_tickets(changeset, review, status_changed)
                    tickets = review.tickets
                req.redirect(req.href(req.path_info, tickets=tickets))
            ctx = web_context(req)
            format_summary = functools.partial(format_to_html,
                                               self.env,
                                               ctx,
                                               escape_newlines=True)
            format_time = functools.partial(user_time, req, format_datetime)

            add_stylesheet(req, 'coderev/coderev.css')
            add_script(req, 'coderev/coderev.js')
            add_script_data(
                req, {
                    'review': {
                        'status':
                        review.status,
                        'encoded_status':
                        review.encode(review.status),
                        'summaries': [
                            dict([
                                ('html_summary', format_summary(r['summary'])),
                                ('pretty_when', format_time(r['when'])),
                                ('pretty_timedelta', pretty_timedelta(
                                    r['when'])), ('reviewer', r['reviewer']),
                                ('status', r['status'])
                            ])
                            for r in CodeReview.select(self.env, reponame, rev)
                        ],
                    },
                    'tickets': tickets,
                    'statuses': self.statuses,
                    'form_token': req.form_token,
                })
            req.send_header('Cache-Control', 'no-cache')
        elif req.path_info.startswith('/ticket/'):
            add_stylesheet(req, 'coderev/coderev.css')
        return template, data, content_type

    # Private methods

    def _update_tickets(self, changeset, review, status_changed):
        """Updates the tickets referenced by the given review's changeset
        with a comment of field changes.  Field changes and command execution
        may occur if specified in trac.ini and the review's changeset is the
        last one of the ticket."""
        status = review.encode(review.status).lower()

        # build comment
        comment = None
        if status_changed or review['summary']:
            if status_changed:
                comment = "Code review set to %s" % review['status']
            else:
                comment = "Code review comment"
            repos = changeset.repos
            ref = review.changeset
            disp_ref = str(repos.short_rev(review.changeset))
            if review.repo:
                ref += '/' + review.repo
                disp_ref += '/' + review.repo
            comment += ' for [changeset:"%s" %s]' % (ref, disp_ref)
            if review['summary']:
                comment += ":\n\n%s" % review['summary']

        invoked = False
        for ticket in review.tickets:
            tkt = Ticket(self.env, ticket)

            # determine ticket changes
            changes = {}
            if self._is_complete(ticket, review, failed_ok=True):
                changes = self._get_ticket_changes(tkt, status)
            # update ticket if there's a review summary or ticket changes
            if comment or changes:
                for field, value in changes.items():
                    tkt[field] = value
                tkt.save_changes(review['reviewer'], comment)

            # check to invoke command
            if not invoked and self._is_complete(ticket, review):
                self._execute_command()
                invoked = True

    def _is_complete(self, ticket, review, failed_ok=False):
        """Returns True if the ticket is complete (or only the last review
        failed if ok_failed is True) and therefore actions (e.g., ticket
        changes and executing commands) should be taken.

        A ticket is complete when its completeness criteria is met and
        the review has PASSED and is the ticket's last review with no
        other PENDING reviews.  Completeness criteria is defined in
        trac.ini like this:

         completeness = phase=(codereview|verifying|releasing)

        The above means that the ticket's phase field must have a value
        of either codereview, verifying, or releasing for the ticket to
        be considered complete.  This helps prevent actions from being
        taken if there's a code review of partial work before the ticket
        is really ready to be fully tested and released.
        """
        # check review's completeness
        reason = is_incomplete(self.env, review, ticket)
        if failed_ok and reason and CodeReview.NOT_PASSED in reason:
            return True
        return not reason

    def _get_ticket_changes(self, tkt, status):
        """Return a dict of field-value pairs of ticket fields to change
        for the given ticket as defined in trac.ini.  As one workflow
        opinion, the changes are processed in order:

         passed = phase=verifying,owner={captain}

        In the above example, if the review passed and the ticket's phase
        already = verifying, then the owner change will not be included.
        """
        changes = {}
        for group in getattr(self, status, []):
            if '=' not in group:
                continue
            field, value = group.split('=', 1)
            if value.startswith('{'):
                value = tkt[value.strip('{}')]
            if tkt[field] == value:
                break  # no more changes once ticket already has target value
            changes[field] = value
        return changes

    def _execute_command(self):
        if not self.command:
            return
        p = Popen(self.command, shell=True, stderr=STDOUT, stdout=PIPE)
        out = p.communicate()[0]
        if p.returncode == 0:
            self.log.info('command: %s', self.command)
        else:
            self.log.error('command error: %s\n%s', self.command, out)
コード例 #8
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',
        """Enable or disable components provided by Trac and plugins.
        The component to enable/disable is specified by the option name.
        The enabled state 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
        component 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:
        {{{#!ini
        [components]
        trac.ticket.report.ReportModule = disabled
        acct_mgr.* = enabled
        }}}

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

        To view the list of active components, go to the ''Plugins''
        section of ''About Trac'' (requires `CONFIG_VIEW`
        [TracPermissions permission]).

        See also: TracPlugins
        """)

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

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

        Non-absolute paths are relative to the Environment `conf`
        directory.
        """)

    base_url = Option(
        'trac', 'base_url', '', """Base URL of the Trac site.

        This is used to produce documents outside of the web browsing
        context, such as URLs in notification e-mails that point to
        Trac resources.
        """)

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

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

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

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

    anonymous_session_lifetime = IntOption(
        'trac', 'anonymous_session_lifetime', '90',
        """Lifetime of the anonymous session, in days.

        Set the option to 0 to disable purging old anonymous sessions.
        (''since 1.0.17'')""")

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

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

    project_url = Option(
        'project', 'url', '', """URL of the project web site.

        This is usually the domain in which the `base_url` resides.
        For example, the project URL might be !https://myproject.com,
        with the Trac site (`base_url`) residing at either
        !https://trac.myproject.com or !https://myproject.com/trac.
        The project URL is added to the footer of 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="https://trac.edgewall.org/">'
           'https://trac.edgewall.org/</a>'),
        """Page footer text (right-aligned).""")

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

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

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

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

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

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

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

        If nothing is set, the following will be used:

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

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

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

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

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

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

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

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

        if create:
            self.create(options, default_data)
            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()))

    # 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, str):
            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().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:
            with open(os.path.join(self.path, 'VERSION'),
                      encoding='utf-8') as f:
                tag = f.readline().rstrip()
        except Exception as e:
            raise TracError(
                _("No Trac environment found at %(path)s\n"
                  "%(e)s",
                  path=self.path,
                  e=exception_to_unicode(e)))
        if tag != _VERSION:
            raise TracError(
                _("Unknown Trac environment type '%(type)s'", type=tag))

    @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=[], default_data=True):
        """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 https://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
        dbm = DatabaseManager(self)
        dbm.init_db()
        if default_data:
            dbm.insert_default_data()

    @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 a 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.normcase(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.encode('utf-8')).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 an 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:
            try:
                with self.component_guard(participant, reraise=True):
                    if participant.environment_needs_upgrade():
                        self.log.warning(
                            "Component %s requires an environment upgrade",
                            participant)
                        return True
            except Exception as e:
                raise TracError(
                    _(
                        "Unable to check for upgrade of "
                        "%(module)s.%(name)s: %(err)s",
                        module=participant.__class__.__module__,
                        name=participant.__class__.__name__,
                        err=exception_to_unicode(e)))
        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("[trac] base_url option not set in "
                             "configuration, generated links may be incorrect")
        return Href(self.base_url)

    def _update_sample_config(self):
        filename = os.path.join(self.config_file_path + '.sample')
        if not os.path.isfile(filename):
            return
        config = Configuration(filename)
        config.set_defaults()
        try:
            config.save()
        except EnvironmentError as e:
            self.log.warning("Couldn't write sample configuration file (%s)%s",
                             e, exception_to_unicode(e, traceback=True))
        else:
            self.log.info(
                "Wrote sample configuration file with the new "
                "settings and their default values: %s", filename)
コード例 #9
0
class CommitTicketUpdater(Component):
    """Update tickets based on commit messages.

    This component hooks into changeset notifications and searches commit
    messages for text in the form of:
    {{{
    command #1
    command #1, #2
    command #1 & #2
    command #1 and #2
    }}}

    Instead of the short-hand syntax "#1", "ticket:1" can be used as well,
    e.g.:
    {{{
    command ticket:1
    command ticket:1, ticket:2
    command ticket:1 & ticket:2
    command ticket:1 and ticket:2
    }}}

    Using the long-form syntax allows a comment to be included in the
    reference, e.g.:
    {{{
    command ticket:1#comment:1
    command ticket:1#comment:description
    }}}

    In addition, the ':' character can be omitted and issue or bug can be used
    instead of ticket.

    You can have more than one command in a message. The following commands
    are supported. There is more than one spelling for each command, to make
    this as user-friendly as possible.

      close, closed, closes, fix, fixed, fixes::
        The specified tickets are closed, and the commit message is added to
        them as a comment.

      references, refs, addresses, re, see::
        The specified tickets are left in their current status, and the commit
        message is added to them as a comment.

    A fairly complicated example of what you can do is with a commit message
    of:

        Changed blah and foo to do this or that. Fixes #10 and #12,
        and refs #12.

    This will close #10 and #12, and add a note to #12.
    """

    implements(IRepositoryChangeListener)

    envelope = Option(
        'ticket', 'commit_ticket_update_envelope', '',
        """Require commands to be enclosed in an envelope.

        Must be empty or contain two characters. For example, if set to `[]`,
        then commands must be in the form of `[closes #4]`.""")

    commands_close = Option(
        'ticket', 'commit_ticket_update_commands.close',
        'close closed closes fix fixed fixes',
        """Commands that close tickets, as a space-separated list.""")

    commands_refs = Option(
        'ticket', 'commit_ticket_update_commands.refs',
        'addresses re references refs see',
        """Commands that add a reference, as a space-separated list.

        If set to the special value `<ALL>`, all tickets referenced by the
        message will get a reference to the changeset.""")

    check_perms = BoolOption(
        'ticket', 'commit_ticket_update_check_perms', 'true',
        """Check that the committer has permission to perform the requested
        operations on the referenced tickets.

        This requires that the user names be the same for Trac and repository
        operations.""")

    notify = BoolOption(
        'ticket', 'commit_ticket_update_notify', 'true',
        """Send ticket change notification when updating a ticket.""")

    ticket_prefix = '(?:#|(?:ticket|issue|bug)[: ]?)'
    ticket_reference = ticket_prefix + \
                       '[0-9]+(?:#comment:([0-9]+|description))?'
    ticket_command = (r'(?P<action>[A-Za-z]*)\s*.?\s*'
                      r'(?P<ticket>%s(?:(?:[, &]*|[ ]?and[ ]?)%s)*)' %
                      (ticket_reference, ticket_reference))

    @property
    def command_re(self):
        begin, end = (re.escape(self.envelope[0:1]),
                      re.escape(self.envelope[1:2]))
        return re.compile(begin + self.ticket_command + end)

    ticket_re = re.compile(ticket_prefix + '([0-9]+)')

    _last_cset_id = None

    # IRepositoryChangeListener methods

    def changeset_added(self, repos, changeset):
        if self._is_duplicate(changeset):
            return
        tickets = self._parse_message(changeset.message)
        comment = self.make_ticket_comment(repos, changeset)
        self._update_tickets(tickets, changeset, comment, datetime.now(utc))

    def changeset_modified(self, repos, changeset, old_changeset):
        if self._is_duplicate(changeset):
            return
        tickets = self._parse_message(changeset.message)
        old_tickets = {}
        if old_changeset is not None:
            old_tickets = self._parse_message(old_changeset.message)
        tickets = dict(each for each in tickets.iteritems()
                       if each[0] not in old_tickets)
        comment = self.make_ticket_comment(repos, changeset)
        self._update_tickets(tickets, changeset, comment, datetime.now(utc))

    def _is_duplicate(self, changeset):
        # Avoid duplicate changes with multiple scoped repositories
        cset_id = (changeset.rev, changeset.message, changeset.author,
                   changeset.date)
        if cset_id != self._last_cset_id:
            self._last_cset_id = cset_id
            return False
        return True

    def _parse_message(self, message):
        """Parse the commit message and return the ticket references."""
        cmd_groups = self.command_re.finditer(message)
        functions = self._get_functions()
        tickets = {}
        for m in cmd_groups:
            cmd, tkts = m.group('action', 'ticket')
            func = functions.get(cmd.lower())
            if not func and self.commands_refs.strip() == '<ALL>':
                func = self.cmd_refs
            if func:
                for tkt_id in self.ticket_re.findall(tkts):
                    tickets.setdefault(int(tkt_id), []).append(func)
        return tickets

    def make_ticket_comment(self, repos, changeset):
        """Create the ticket comment from the changeset data."""
        rev = changeset.rev
        revstring = str(rev)
        drev = str(repos.display_rev(rev))
        if repos.reponame:
            revstring += '/' + repos.reponame
            drev += '/' + repos.reponame
        return """\
In [changeset:"%s" %s]:
{{{
#!CommitTicketReference repository="%s" revision="%s"
%s
}}}""" % (revstring, drev, repos.reponame, rev, changeset.message.strip())

    def _update_tickets(self, tickets, changeset, comment, date):
        """Update the tickets with the given comment."""
        authname = self._authname(changeset)
        perm = PermissionCache(self.env, authname)
        for tkt_id, cmds in tickets.iteritems():
            try:
                self.log.debug("Updating ticket #%d", tkt_id)
                save = False
                with self.env.db_transaction:
                    ticket = Ticket(self.env, tkt_id)
                    ticket_perm = perm(ticket.resource)
                    for cmd in cmds:
                        if cmd(ticket, changeset, ticket_perm) is not False:
                            save = True
                    if save:
                        ticket.save_changes(authname, comment, date)
                if save:
                    self._notify(ticket, date, changeset.author, comment)
            except Exception as e:
                self.log.error(
                    "Unexpected error while processing ticket "
                    "#%s: %s", tkt_id, exception_to_unicode(e))

    def _notify(self, ticket, date, author, comment):
        """Send a ticket update notification."""
        if not self.notify:
            return
        event = TicketChangeEvent('changed', ticket, date, author, comment)
        try:
            NotificationSystem(self.env).notify(event)
        except Exception as e:
            self.log.error(
                "Failure sending notification on change to "
                "ticket #%s: %s", ticket.id, exception_to_unicode(e))

    def _get_functions(self):
        """Create a mapping from commands to command functions."""
        functions = {}
        for each in dir(self):
            if not each.startswith('cmd_'):
                continue
            func = getattr(self, each)
            for cmd in getattr(self, 'commands_' + each[4:], '').split():
                functions[cmd] = func
        return functions

    def _authname(self, changeset):
        """Returns the author of the changeset, normalizing the casing if
        [trac] ignore_author_case is true."""
        return changeset.author.lower() \
               if self.env.config.getbool('trac', 'ignore_auth_case') \
               else changeset.author

    # Command-specific behavior
    # The ticket isn't updated if all extracted commands return False.

    def cmd_close(self, ticket, changeset, perm):
        authname = self._authname(changeset)
        if self.check_perms and not 'TICKET_MODIFY' in perm:
            self.log.info("%s doesn't have TICKET_MODIFY permission for #%d",
                          authname, ticket.id)
            return False
        ticket['status'] = 'closed'
        ticket['resolution'] = 'fixed'
        if not ticket['owner']:
            ticket['owner'] = authname

    def cmd_refs(self, ticket, changeset, perm):
        if self.check_perms and not 'TICKET_APPEND' in perm:
            self.log.info("%s doesn't have TICKET_APPEND permission for #%d",
                          self._authname(changeset), ticket.id)
            return False
コード例 #10
0
ファイル: mail.py プロジェクト: minimalistduck/trac
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())
コード例 #11
0
ファイル: mail.py プロジェクト: minimalistduck/trac
class SmtpEmailSender(Component):
    """E-mail sender connecting to an SMTP server."""

    implements(IEmailSender)

    smtp_server = Option('notification', 'smtp_server', 'localhost',
        """SMTP server hostname to use for email notifications.""")

    smtp_port = IntOption('notification', 'smtp_port', 25,
        """SMTP server port to use for email notification.""")

    smtp_user = Option('notification', 'smtp_user', '',
        """Username for authenticating with SMTP server.""")

    smtp_password = Option('notification', 'smtp_password', '',
        """Password for authenticating with SMTP server.""")

    use_tls = BoolOption('notification', 'use_tls', 'false',
        """Use SSL/TLS to send notifications over SMTP.""")

    def send(self, from_addr, recipients, message):
        global local_hostname
        # Ensure the message complies with RFC2822: use CRLF line endings
        message = fix_eol(message, CRLF)

        self.log.info("Sending notification through SMTP at %s:%d to %s",
                      self.smtp_server, self.smtp_port, recipients)
        try:
            server = smtplib.SMTP(self.smtp_server, self.smtp_port,
                                  local_hostname)
            local_hostname = server.local_hostname
        except smtplib.socket.error as e:
            raise ConfigurationError(
                tag_("SMTP server connection error (%(error)s). Please "
                     "modify %(option1)s or %(option2)s in your "
                     "configuration.",
                     error=to_unicode(e),
                     option1=tag.code("[notification] smtp_server"),
                     option2=tag.code("[notification] smtp_port")))
        # server.set_debuglevel(True)
        if self.use_tls:
            server.ehlo()
            if 'starttls' not in server.esmtp_features:
                raise TracError(_("TLS enabled but server does not support"
                                  " TLS"))
            server.starttls()
            server.ehlo()
        if self.smtp_user:
            server.login(self.smtp_user.encode('utf-8'),
                         self.smtp_password.encode('utf-8'))
        start = time_now()
        server.sendmail(from_addr, recipients, message)
        t = time_now() - start
        if t > 5:
            self.log.warning("Slow mail submission (%.2f s), "
                             "check your mail setup", t)
        if self.use_tls:
            # avoid false failure detection when the server closes
            # the SMTP connection with TLS enabled
            import socket
            try:
                server.quit()
            except socket.sslerror:
                pass
        else:
            server.quit()
コード例 #12
0
ファイル: ticketnav.py プロジェクト: pombredanne/trachacks
class HtmlContent(Component):
    """DEPRECATED

'''Deprecated - use [http://ckeditor.com/ CKEditor]-Plugin instead! '''
Enables HTML content in description, adding Javascript editor and 
adding additional CSS file for manipulation CSS declarations.

Options:
|| '''option name''' || '''values''' || '''description''' ||
|| description_format || `wiki` | `html` || format for ticket description (default: '''wiki''') ||
|| editor_source || valid path || Usually it should stored in project or common js folder. For ckeditor for example it could be site/js/ckeditor/ckeditor.js. ||
|| editor_replace || valid path || Javascript, which should replace textareas. ||
|| additional_css || valid path || Path to additional css file, which overrides css-declarations. ||

Sample configuration:
{{{
[ticket]
description_format = html
editor_source = site/js/ckeditor/ckeditor.js
editor_replace = <script type="text/javascript">CKEDITOR.replace('@FIELD_NAME@', {toolbar: 'custom'});</script>
additional_css = site/css/add_ticket.css
}}}

In above sample configuration [http://ckeditor.com/ CKEditor] is used as online editor and 
the editor source is located in projects-folder `htdocs/js/ckeditor`,
after each `textarea` with HTML-Option content of `editor_replace` will be added.
The file in option `additional_css` will be added and therefore will override these declarations.

'''Attention:''' To have a correct preview, file `ticket_box.html` has to be edited:
{{{
1 <py:if test="field">
2   <py:choose test="">
3     <py:when test="ticket[field.name] and field.format == 'html'">${wiki_to_html(context, '{{{ \n#!html \n' + ticket[field.name]  + '\n}}}', escape_newlines=preserve_newlines)}</py:when>
4     <py:when test="'rendered' in field">${field.rendered}</py:when>
5     <py:otherwise>${ticket[field.name]}</py:otherwise>
6   </py:choose>
7 </py:if>
}}} 

Line 3 has to be inserted into `py:choose` block in above template-snippet.

{{{
<div py:if="ticket.description" class="searchable" xml:space="preserve">
<py:choose>
    <py:when test="description_format == 'html'">${wiki_to_html(context, '{{{ \n#!html \n' + ticket.description  + '\n}}}', escape_newlines=preserve_newlines)}</py:when>
    <py:otherwise>${wiki_to_html(context, ticket.description, escape_newlines=preserve_newlines)}</py:otherwise>
</py:choose>
</div>
}}}

Also `py:choose` block has to be added into div-block near the end of file (see above template-snippet)."""
    implements(IRequestFilter, ITemplateStreamFilter)

    description_format = Option(
        'ticket', 'description_format', '', """Format of description.
        Empty or wiki is Trac Standard; html formats description as HTML.""")

    additional_css_file = Option(
        'ticket', 'additional_css', '',
        """Path to additional css file, which overrides css-declarations.""")

    editor_source = Option(
        'ticket', 'editor_source', '', """Path to javascript editor.
        Usually it should stored in project or common js folder.
        For ckeditor for example it could be site/js/ckeditor/ckeditor.js.""")

    editor_replace = Option('ticket', 'editor_replace', '',
                            """Javascript, which should replace textareas.""")

    def pre_process_request(self, req, handler):
        return handler

    def post_process_request(self, req, template, data, content_type):
        if data and template == 'ticket.html':
            data['description_format'] = self.description_format
            if self.editor_source:
                add_script(req, self.editor_source)
            if self.additional_css_file:
                add_stylesheet(req, self.additional_css_file)
        return template, data, content_type

    def filter_stream(self, req, method, filename, stream, data):
        if filename == 'ticket.html' and self.editor_source and self.editor_replace:
            self.log.debug(
                "further processing: template %s, editor-source %s, editor-replace %s"
                % (filename, self.editor_source, self.editor_replace))

            # check if description should be in HTML
            if self.description_format == "html":
                add_editor = self.editor_replace.replace(
                    "@FIELD_NAME@", "field_description")
                html = HTML(add_editor)
                self.log.debug("add_editor is %s" % add_editor)
                stream |= Transformer(
                    './/textarea[@name="field_description"]').after(html)
            fields = data['fields']
            for f in fields:
                if f['skip'] or not f['type'] == 'textarea' or not f.has_key(
                        'format') or not f['format'] == 'html':
                    continue
                # only textarea-fields with format HTML should be processed at this point
                field_name = 'field_%s' % f['name']
                add_editor = self.editor_replace.replace(
                    "@FIELD_NAME@", field_name)
                html = HTML(add_editor)
                tr_str = './/textarea[@name="%s"]' % field_name
                self.log.debug("add_editor for field %s is %s; tr_str is %s" %
                               (field_name, add_editor, tr_str))
                stream |= Transformer(tr_str).after(html)

        return stream


#===============================================================================
# UNUSED COMPONENTS
# These components are not used yet, because they are not working and
# are only made because in a research manner.
#===============================================================================

#===============================================================================
# class DisplayDate(Component):
#    """DEPRECATED
#
# '''Deprecated - use trac itself with revision 10629 or later! '''
# (see Trac-Ticket [http://trac.edgewall.org/ticket/9777 #9777])
# Displays date next to summary as absolute date instead of timedelta.
# Timedelta will be displayed only as title.
#
# '''Attention''': This works partly fine in German and English. Other languages / locales haven't been tested.
# It doesn't set the language correct in changes (like comments / attachments).
# You also have to edit your `template.htm` and place it under your project folder.
#
# Sample change:
# {{{
#  <div class="date">
#    <py:choose>
#        <py:when test="date_format == 'absolute'">
#            <p i18n:msg="created" py:if="ticket.exists">Creation date: ${dateinfo_abs(ticket.time)}</p>
#            <p i18n:msg="modified" py:if="ticket.changetime != ticket.time">Modify date: ${dateinfo_abs(ticket.changetime)}</p>
#        </py:when>
#        <py:otherwise>
#            <p i18n:msg="created" py:if="ticket.exists">Opened ${dateinfo(ticket.time)} ago</p>
#            <p i18n:msg="modified" py:if="ticket.changetime != ticket.time">Last modified ${dateinfo(ticket.changetime)} ago</p>
#        </py:otherwise>
#    </py:choose>
#    <p py:if="not ticket.exists"><i>(ticket not yet created)</i></p>
#  </div>
# }}}
#
# Options:
# || '''option name''' || '''values''' || '''description''' ||
# || date_format || `relative` | `absolute` || format of date in ticket view (default: '''relative''') ||
#
# Sample configuration:
# {{{
# [ticket]
# date_format = absolute
# }}}
#
# You might restart your webserver after installing this plugin, since it doesn't display the date correctly.
# """
#    implements (IRequestFilter, ITemplateStreamFilter)
#
#    date_format = Option('ticket', 'date_format', 'relative',
#        """Format of date in ticket view.
#        Empty or relative is Trac Standard; absolute formats date as date/time.""")
#
#    def __init__(self):
#        locale_dir = pkg_resources.resource_filename(__name__, 'locale')
#        add_domain(self.env.path, locale_dir)
#
#    def pre_process_request(self, req, handler):
#        return handler
#
#    def post_process_request(self, req, template, data, content_type):
#        if data and template == 'ticket.html':
#            self.log.info("[post_process_request] called this method with data with template: %s" % template)
#            def dateinfo_abs(date):
#                self.log.info("[post_process_request] called method 'dateinfo'")
#                return tag.span(format_datetime(date),
#                                title=pretty_timedelta(date))
#            data['date_format'] = self.date_format
#            if self.date_format == "absolute":
#                data['dateinfo_abs'] = dateinfo_abs
#
#            self.log.debug("data %r" % data)
#        return template, data, content_type
#
#    def filter_stream(self, req, method, filename, stream, data):
# #        if not self.date_format == "absolute":
# #            return stream
# #
# #        if filename == 'ticket.html':
# #            self.log.debug( "replacing relative datetime to absolute datetime" )
# #            # just a hack (works at least well in German and English)
# #            def replace_descr(stream):
# #                for mark, (kind, data, pos) in stream:
# #                    if mark and kind is TEXT:
# #                        test = data.upper()
# #                        if re.match(r'[0-9]{2}', test):
# #                            yield mark, (kind, data, pos)
# #                        else:
# #                            yield mark, (kind, '', pos)
# #                    else:
# #                        yield mark, (kind, data, pos)
# #
# #            stream =  stream | Transformer('.//div[@id="ticket"]/div[@class="date"]/p[1]') \
# #                            .apply( replace_descr ).prepend( _('Creation date') + ': ' )
# #            stream =  stream | Transformer('.//div[@id="ticket"]/div[@class="date"]/p[2]') \
# #                            .apply( replace_descr ).prepend( _('Modify date') + ': ' )
# #
#            # just a hack, which works in German, but not in English!
# #            def replace_comment_descr(stream):
# #                do_replace_text = 0
# #                for mark, (kind, data, pos) in stream:
# #                    if mark and kind is END:
# #                        try:
# #                            if data.localname == "h3":
# #                                do_replace_text = 0
# #                            elif data.localname == "span":
# #                                do_replace_text += 1
# #                        except Exception, e:
# #                            print "*** EXEPTION *** %s" % e
# #                        yield mark, (kind, data, pos)
# #                    elif mark and kind is TEXT:
# #                        test = data.upper()
# #                        if re.match(r'[0-9]{2}', test):
# #                            yield mark, (kind, data, pos)
# #                        elif do_replace_text == 2:
# #                            yield mark, (kind, '', pos)
# #                        else:
# #                            yield mark, (kind, data, pos)
# #                    else:
# #                        yield mark, (kind, data, pos)
# #
# #            stream =  stream | Transformer('.//h3 [@class="change"]') \
# #                            .apply( replace_comment_descr ).prepend( _('Modify date') + ': ' )
#        return stream
#===============================================================================

#===============================================================================
#class FileUploader(Component):
#    """Test for uploading images by CKEditor"""
#    implements (IAttachmentChangeListener, IAttachmentManipulator, IRequestHandler)
#
#    def match_request(self, req):
#        """Return whether the handler wants to process the given request."""
#        return re.match(r'/image_upload', req.path_info)
#
#    def process_request(self, req):
#        """Process the request.
#
#        For ClearSilver, return a `(template_name, content_type)` tuple,
#        where `template` is the ClearSilver template to use (either a
#        `neo_cs.CS` object, or the file name of the template), and
#        `content_type` is the MIME type of the content.
#
#        For Genshi, return a `(template_name, data, content_type)` tuple,
#        where `data` is a dictionary of substitutions for the template.
#
#        For either templating systems, "text/html" is assumed if `content_type`
#        is `None`.
#
#        Note that if template processing should not occur, this method can
#        simply send the response itself and not return anything.
#        """
#        print "========= ____  [process_request]"
#        if ( req.path_info == "/image_upload" ):
#            add_stylesheet(req, 'hw/css/style.css')
#            print "========= ____  [process_request] correct path ... doing more"
#            data = {}
#            return 'ticketnav/image-uploader.html', data, None
#
#    def prepare_attachment(self, req, attachment, fields):
#        """Not currently called, but should be provided for future
#        compatibility."""
#        print "--------------.... [prepare_attachment] try to add attachment"
#
#    def validate_attachment(self, req, attachment):
#        """Validate an attachment after upload but before being stored in Trac
#        environment.
#
#        Must return a list of `(field, message)` tuples, one for each problem
#        detected. `field` can be any of `description`, `username`, `filename`,
#        `content`, or `None` to indicate an overall problem with the
#        attachment. Therefore, a return value of `[]` means everything is
#        OK."""
#        print "_______________.... [validate_attachment] try to add attachment"
#        return []
#
#    def attachment_added(self, attachment):
#        """Called when an attachment is added."""
#        print "_______________ [attachment_added] try to add attachment"
#        print "_______________ [attachment_added] add attachment: %s" % attachment
##        return attachment
#
#    def attachment_deleted(self, attachment):
#        """Called when an attachment is deleted."""
#        print "_______________ [attachment_deleted] try to delete attachment"
##        return attachment
#
#    def attachment_reparented(self, attachment, old_parent_realm, old_parent_id):
#        """Called when an attachment is reparented."""
#        print "_______________ [attachment_reparented] add attachment: %s" % attachment
#        return attachment, old_parent_realm, old_parent_id
#===============================================================================

#===============================================================================
# Tested if copying ticket-box.html is working, but it is not!
#===============================================================================
#class HtmlContent(Component):
#    implements (IRequestFilter)
#    """Allow description and other textarea-fields having HTML-content"""
#
#    # IRequestHandler methods
##    def match_request(self, req):
##        print "===== HtmlContent, path_info: %s" % req.path_info
##        return re.match(r'/(ticket|newticket)(?:_trac)?(?:/.*)?$', req.path_info)
#
#    def pre_process_request(self, req, handler):
#        return handler
#
#    # IRequestHandler methods
#    def post_process_request(self, req, template, data, content_type):
#        print "template: %s" % template
#        self._check_init()
##        data = {}
##        return handler
##        return 'ticket.html', data, None
#        return template, data, content_type
#
#
#    def _check_init(self):
#        """First check if Plugin has already been initialized.
#        """
#
#        print "====== _check_init"
#
#        template_path = self.env.path
#
#        if template_path and template_path.endswith('/'):
#            template_path += 'templates'
#        else:
#            template_path += '/templates'
#
#        if os.access(template_path, os.W_OK):
#            print "can write to path %s" % template_path
#            from pkg_resources import resource_filename
##            src_name = resource_filename(__name__, 'templates/ticket-box.html')
#            src_name = resource_filename(__name__, 'templates')
#            print "src_name: %s" % src_name
#            shutil.copy(src_name + '/ticket-box.html', template_path + '/ticket-box.html')
#        elif os.access(template_path, os.R_OK):
#            print "can read to path %s" % template_path
#
#        print "template_path: %s" % template_path
コード例 #13
0
ファイル: ticketnav.py プロジェクト: pombredanne/trachacks
class TextAreaDescription(Component):
    """Shows next to a text area (like description itself or any custom description) 
an description for what the field is for.

Default values for options:
{{{
[ticket]
description_descr = 
descr_template = <div style="white-space: normal; height: 250px; overflow:scroll;" class="system-message">%s<div>
}}}
    """
    implements(ITemplateStreamFilter, IRequestFilter, ITemplateProvider)

    description_descr = Option('ticket', 'description_descr', '',
                               """Explaination of description.""")
    descr_template = Option(
        'ticket', 'descr_template',
        '<div style="white-space: normal; width: 250px; height: 250px; overflow:scroll;" class="%s">%s<div>',
        """Explaination of description.""")

    def filter_stream(self, req, method, filename, stream, data):
        if filename == 'ticket.html':
            #            print "_____________ am in TextAreaDescription"
            fields = data['fields']
            if self.description_descr:
                #                print "having description_descr: %s" % self.description_descr
                #                print "having description_template: %s" % self.descr_template
                html_d = self.descr_template % ('ticket-rndescr',
                                                self.description_descr)
                stream |= Transformer(
                    './/th/label[@for="field-description"]').after(
                        HTML(html_d))

            for f in fields:
                if f['skip'] or not f[
                        'type'] == 'textarea':  # or not f.has_key('descr'):
                    continue

                descr = self.config.get('ticket-custom',
                                        '%s.descr' % f['name'])
                if descr:
                    #                    print "processing field %s" % f
                    css_class = self.config.get('ticket-custom',
                                                '%s.css_class' % f['name'])
                    #                    print css_class
                    field_name = 'field-%s' % f['name']
                    tr_str = './/label[@for="%s"]' % field_name
                    html = self.descr_template % (css_class, descr)
                    stream |= Transformer(tr_str).after(HTML(html))
        return stream

    # IRequestHandler methods
    def pre_process_request(self, req, handler):
        return handler

    #===========================================================================
    # Add JavaScript an an additional css to ticket template
    #===========================================================================


#    def post_process_request(self, req, template, data, content_type):
#        if req.path_info .startswith('/newticket')or \
#            req.path_info .startswith('/ticket'):
#            add_stylesheet(req, 'ticketnav/css/ticket_descr.css')
#        return template, data, content_type

    def post_process_request(self, req, template, data, content_type):
        if req.path_info .startswith('/newticket') or \
            req.path_info .startswith('/ticket'):
            add_stylesheet(req, 'hw/css/ticket_descr.css')
        return template, data, content_type

    def get_templates_dirs(self):
        return  #[resource_filename(__name__, 'templates')]

    def get_htdocs_dirs(self):
        #        self.log.debug('TextAreaDescription. get_htdocs_dirs: %s' % resource_filename(__name__, 'htdocs') )
        return [('hw', resource_filename(__name__, 'htdocs'))]
コード例 #14
0
ファイル: web_ui.py プロジェクト: pombredanne/trachacks
class LoginModule(auth.LoginModule, CommonTemplateProvider):
    """Custom login form and processing.

    This is woven with the trac.auth.LoginModule it inherits and overwrites.
    But both can't co-exist, so Trac's built-in authentication module
    must be disabled to use this one.
    """

    # Trac core options, replicated here to not make them disappear by
    # disabling auth.LoginModule.
    check_ip = BoolOption(
        'trac', 'check_auth_ip', 'false',
        """Whether the IP address of the user should be checked for
         authentication (''since 0.9'').""")

    ignore_case = BoolOption(
        'trac', 'ignore_auth_case', 'false',
        """Whether login names should be converted to lower case
        (''since 0.9'').""")

    auth_cookie_lifetime = IntOption(
        'trac', 'auth_cookie_lifetime', 0,
        """Lifetime of the authentication cookie, in seconds.
        
        This value determines how long the browser will cache
        authentication information, and therefore, after how much
        inactivity a user will have to log in again. The default value
        of 0 makes the cookie expire at the end of the browsing
        session. (''since 0.12'')""")

    auth_cookie_path = Option(
        'trac', 'auth_cookie_path', '',
        """Path for the authentication cookie. Set this to the common
        base path of several Trac instances if you want them to share
        the cookie.  (''since 0.12'')""")

    # Options dedicated to acct_mgr.web_ui.LoginModule.
    login_opt_list = BoolOption(
        'account-manager', 'login_opt_list', False,
        """Set to True, to switch login page style showing alternative actions
        in a single listing together.""")

    cookie_refresh_pct = IntOption(
        'account-manager', 'cookie_refresh_pct', 10,
        """Persistent sessions randomly get a new session cookie ID with
        likelihood in percent per work hour given here (zero equals to never)
        to decrease vulnerability of long-lasting sessions.""")

    environ_auth_overwrite = BoolOption(
        'account-manager', 'environ_auth_overwrite', True,
        """Whether environment variable REMOTE_USER should get overwritten
        after processing login form input. Otherwise it will only be set,
        if unset at the time of authentication.""")

    # Update cookies for persistant sessions only 1/day.
    #   hex_entropy returns 32 chars per call equal to 128 bit of entropy,
    #   so it should be technically impossible to explore the hash even within
    #   a year by just throwing forged HTTP requests at the server.
    #   I.e. it would require 1.000.000 machines, each at 5*10^24 requests/s,
    #   equal to a full-scale DDoS attack - an entirely different issue.
    UPDATE_INTERVAL = 86400

    def __init__(self):
        cfg = self.config
        if is_enabled(self.env, self.__class__) and \
                is_enabled(self.env, auth.LoginModule):
            # Disable auth.LoginModule to handle login requests alone.
            self.env.log.info("Concurrent enabled login modules found, "
                              "fixing configuration ...")
            cfg.set('components', 'trac.web.auth.loginmodule', 'disabled')
            # Changes are intentionally not written to file for persistence.
            # This could cause the environment to reload a bit too early, even
            # interrupting a rewrite in progress by another thread and causing
            # a DoS condition by truncating the configuration file.
            self.env.log.info("trac.web.auth.LoginModule disabled, "
                              "giving preference to %s." % self.__class__)

        self.cookie_lifetime = self.auth_cookie_lifetime
        if not self.cookie_lifetime > 0:
            # Set the session to expire after some time and not
            #   when the browser is closed - what is Trac core default).
            self.cookie_lifetime = 86400 * 30  # AcctMgr default = 30 days

    def authenticate(self, req):
        if req.method == 'POST' and req.path_info.startswith('/login') and \
                req.args.get('user_locked') is None:
            username = self._remote_user(req)
            acctmgr = AccountManager(self.env)
            guard = AccountGuard(self.env)
            if guard.login_attempt_max_count > 0:
                if username is None:
                    # Get user for failed authentication attempt.
                    f_user = req.args.get('username')
                    req.args['user_locked'] = False
                    # Log current failed login attempt.
                    guard.failed_count(f_user, req.remote_addr)
                    if guard.user_locked(f_user):
                        # Step up lock time prolongation only while locked.
                        guard.lock_count(f_user, 'up')
                        req.args['user_locked'] = True
                elif guard.user_locked(username):
                    req.args['user_locked'] = True
                    # Void successful login as long as user is locked.
                    username = None
                else:
                    req.args['user_locked'] = False
                    if req.args.get('failed_logins') is None:
                        # Reset failed login attempts counter.
                        req.args['failed_logins'] = guard.failed_count(
                            username, reset=True)
            else:
                req.args['user_locked'] = False
            if not 'REMOTE_USER' in req.environ or self.environ_auth_overwrite:
                if 'REMOTE_USER' in req.environ:
                    # Complain about another component setting environment
                    # variable for authenticated user.
                    self.env.log.warn("LoginModule.authenticate: "
                                      "'REMOTE_USER' was set to '%s'" %
                                      req.environ['REMOTE_USER'])
                self.env.log.debug("LoginModule.authenticate: Set "
                                   "'REMOTE_USER' = '%s'" % username)
                req.environ['REMOTE_USER'] = username
        return auth.LoginModule.authenticate(self, req)

    authenticate = if_enabled(authenticate)

    match_request = if_enabled(auth.LoginModule.match_request)

    def process_request(self, req):
        if req.path_info.startswith('/login') and req.authname == 'anonymous':
            try:
                referer = self._referer(req)
            except AttributeError:
                # Fallback for Trac 0.11 compatibility.
                referer = req.get_header('Referer')
            # Steer clear of requests going nowhere or loop to self.
            if referer is None or \
                    referer.startswith(str(req.abs_href()) + '/login'):
                referer = req.abs_href()
            data = {
                '_dgettext':
                dgettext,
                'login_opt_list':
                self.login_opt_list,
                'persistent_sessions':
                AccountManager(self.env).persistent_sessions,
                'referer':
                referer,
                'registration_enabled':
                RegistrationModule(self.env).enabled,
                'reset_password_enabled':
                AccountModule(self.env).reset_password_enabled
            }
            if req.method == 'POST':
                self.log.debug(
                    "LoginModule.process_request: 'user_locked' = %s" %
                    req.args.get('user_locked'))
                if not req.args.get('user_locked'):
                    # TRANSLATOR: Intentionally obfuscated login error
                    data['login_error'] = _("Invalid username or password")
                else:
                    f_user = req.args.get('username')
                    release_time = AccountGuard(self.env).pretty_release_time(
                        req, f_user)
                    if not release_time is None:
                        data['login_error'] = _(
                            """Account locked, please try again after
                            %(release_time)s
                            """,
                            release_time=release_time)
                    else:
                        data['login_error'] = _("Account locked")
            return 'login.html', data, None
        else:
            n_plural = req.args.get('failed_logins')
            if n_plural > 0:
                add_warning(
                    req,
                    Markup(
                        tag.span(
                            tag(
                                ngettext(
                                    "Login after %(attempts)s failed attempt",
                                    "Login after %(attempts)s failed attempts",
                                    n_plural,
                                    attempts=n_plural)))))
        return auth.LoginModule.process_request(self, req)

    # overrides
    def _get_name_for_cookie(self, req, cookie):
        """Returns the username for the current Trac session.

        It's called by authenticate() when the cookie 'trac_auth' is sent
        by the browser.
        """

        acctmgr = AccountManager(self.env)
        name = None
        # Replicate _get_name_for_cookie() or _cookie_to_name() since Trac 1.0
        # adding special handling of persistent sessions, as the user may have
        # a dynamic IP adress and this would lead to the user being logged out
        # due to an IP address conflict.
        if 'trac_auth_session' in req.incookie or True:
            db = self.env.get_db_cnx()
            cursor = db.cursor()
            sql = "SELECT name FROM auth_cookie WHERE cookie=%s AND ipnr=%s"
            args = (cookie.value, req.remote_addr)
            if acctmgr.persistent_sessions or not self.check_ip:
                sql = "SELECT name FROM auth_cookie WHERE cookie=%s"
                args = (cookie.value, )
            cursor.execute(sql, args)
            name = cursor.fetchone()
            name = name and name[0] or None
        if name is None:
            self._expire_cookie(req)

        if acctmgr.persistent_sessions and name and \
                'trac_auth_session' in req.incookie and \
                int(req.incookie['trac_auth_session'].value) < \
                int(time.time()) - self.UPDATE_INTERVAL:
            # Persistent sessions enabled, the user is logged in
            # ('name' exists) and has actually decided to use this feature
            # (indicated by the 'trac_auth_session' cookie existing).
            #
            # NOTE: This method is called on every request.

            # Refresh session cookie
            # Update the timestamp of the session so that it doesn't expire.
            self.env.log.debug('Updating session %s for user %s' %
                               (cookie.value, name))
            # Refresh in database
            db = self.env.get_db_cnx()
            cursor = db.cursor()
            cursor.execute(
                """
                UPDATE  auth_cookie
                    SET time=%s
                WHERE   cookie=%s
                """, (int(time.time()), cookie.value))
            db.commit()

            # Change session ID (cookie.value) now and then as it otherwise
            #   never would change at all (i.e. stay the same indefinitely and
            #   therefore is more vulnerable to be hacked).
            if random.random() + self.cookie_refresh_pct / 100.0 > 1:
                old_cookie = cookie.value
                # Update auth cookie value
                cookie.value = hex_entropy()
                self.env.log.debug('Changing session id for user %s to %s' %
                                   (name, cookie.value))
                db = self.env.get_db_cnx()
                cursor = db.cursor()
                cursor.execute(
                    """
                    UPDATE  auth_cookie
                        SET cookie=%s
                    WHERE   cookie=%s
                    """, (cookie.value, old_cookie))
                db.commit()
                if self.auth_cookie_path:
                    self._distribute_auth(req, cookie.value, name)

            cookie_lifetime = self.cookie_lifetime
            cookie_path = self._get_cookie_path(req)
            req.outcookie['trac_auth'] = cookie.value
            req.outcookie['trac_auth']['path'] = cookie_path
            req.outcookie['trac_auth']['expires'] = cookie_lifetime
            req.outcookie['trac_auth_session'] = int(time.time())
            req.outcookie['trac_auth_session']['path'] = cookie_path
            req.outcookie['trac_auth_session']['expires'] = cookie_lifetime
            try:
                if self.env.secure_cookies:
                    req.outcookie['trac_auth']['secure'] = True
                    req.outcookie['trac_auth_session']['secure'] = True
            except AttributeError:
                # Report details about Trac compatibility for the feature.
                self.env.log.debug(
                    """Restricting cookies to HTTPS connections is requested,
                    but is supported only by Trac 0.11.2 or later version.
                    """)
        return name

    # overrides
    def _do_login(self, req):
        if not req.remote_user:
            if req.method == 'GET':
                # Trac before 0.12 has known weak redirect loop protection.
                # Adding redirect fix from Trac 0.12, and especially avert
                # from 'self._redirect_back', when we see a 'GET' here.
                referer = req.get_header('Referer')
                # Steer clear of requests going nowhere or loop to self
                if referer is None or \
                        referer.startswith(str(req.abs_href()) + '/login'):
                    referer = req.abs_href()
                req.redirect(referer)
            self._redirect_back(req)
        res = auth.LoginModule._do_login(self, req)

        cookie_path = self._get_cookie_path(req)
        # Fix for Trac 0.11, that always sets path to `req.href()`.
        req.outcookie['trac_auth']['path'] = cookie_path
        # Inspect current cookie and try auth data distribution for SSO.
        cookie = req.outcookie.get('trac_auth')
        if cookie and self.auth_cookie_path:
            self._distribute_auth(req, cookie.value, req.remote_user)

        if req.args.get('rememberme', '0') == '1':
            # Check for properties to be set in auth cookie.
            cookie_lifetime = self.cookie_lifetime
            req.outcookie['trac_auth']['expires'] = cookie_lifetime

            # This cookie is used to indicate that the user is actually using
            # the "Remember me" feature. This is necessary for
            # '_get_name_for_cookie()'.
            req.outcookie['trac_auth_session'] = 1
            req.outcookie['trac_auth_session']['path'] = cookie_path
            req.outcookie['trac_auth_session']['expires'] = cookie_lifetime
            try:
                if self.env.secure_cookies:
                    req.outcookie['trac_auth_session']['secure'] = True
            except AttributeError:
                # Report details about Trac compatibility for the feature.
                self.env.log.debug(
                    """Restricting cookies to HTTPS connections is requested,
                    but is supported only by Trac 0.11.2 or later version.
                    """)
        else:
            # In Trac 0.12 the built-in authentication module may have already
            # set cookie's expires attribute, so because the user did not
            # check 'remember me' we need to delete it here to ensure that the
            # cookie will still expire at the end of the session.
            try:
                del req.outcookie['trac_auth']['expires']
            except KeyError:
                pass
            # If there is a left-over session cookie from a previous
            # authentication session, expire it now.
            if 'trac_auth_session' in req.incookie:
                self._expire_session_cookie(req)
        return res

    def _distribute_auth(self, req, trac_auth, name=None):
        # Single Sign On authentication distribution between multiple
        #   Trac environments managed by AccountManager.
        local_environ = req.environ.get('SCRIPT_NAME', '').lstrip('/')

        for environ, path in get_environments(req.environ).iteritems():
            if environ != local_environ:
                try:
                    # Cache environment for subsequent invocations.
                    env = open_environment(path, use_cache=True)
                    auth_cookie_path = env.config.get('trac',
                                                      'auth_cookie_path')
                    # Consider only Trac environments with equal, non-default
                    #   'auth_cookie_path', which enables cookies to be shared.
                    if auth_cookie_path == self.auth_cookie_path:
                        db = env.get_db_cnx()
                        cursor = db.cursor()
                        # Authentication cookie values must be unique. Ensure,
                        #   there is no other session (or worst: session ID)
                        #   associated to it.
                        cursor.execute(
                            """
                            DELETE FROM auth_cookie
                            WHERE  cookie=%s
                            """, (trac_auth, ))
                        if not name:
                            db.commit()
                            env.log.debug('Auth data revoked from: %s' %
                                          local_environ)
                            continue
                        cursor.execute(
                            """
                            INSERT INTO auth_cookie
                                   (cookie,name,ipnr,time)
                            VALUES (%s,%s,%s,%s)
                            """,
                            (trac_auth, name, req.remote_addr, int(
                                time.time())))
                        db.commit()
                        env.log.debug('Auth data received from: %s' %
                                      local_environ)
                        self.log.debug('Auth distribution success: %s' %
                                       environ)
                except Exception, e:
                    self.log.debug(
                        'Auth distribution skipped for env %s: %s' %
                        (environ, exception_to_unicode(e, traceback=True)))
コード例 #15
0
class GitolitePermissionManager(Component):
    implements(IAdminPanelProvider, ITemplateProvider)

    gitolite_admin_reponame = Option('trac-gitolite',
                                     'admin_reponame',
                                     default="gitolite-admin")
    gitolite_admin_ssh_path = Option(
        'trac-gitolite',
        'admin_ssh_path',
        default="git@localhost:gitolite-admin.git")
    gitolite_admin_real_reponame = Option('trac-gitolite',
                                          'admin_real_reponame',
                                          default="gitolite-admin")
    gitolite_admin_system_user = Option('trac-gitolite',
                                        'admin_system_user',
                                        default="trac")

    def get_users(self):
        node = utils.get_repo_node(self.env, self.gitolite_admin_reponame,
                                   "keydir")
        assert node.isdir, "Node %s at /keydir/ is not a directory" % node
        for child in node.get_entries():
            name = child.get_name()
            assert name.endswith(".pub"), "Node %s" % name
            name = name[:-4]
            yield name

    def read_config(self):
        node = utils.get_repo_node(self.env, self.gitolite_admin_reponame,
                                   "conf/gitolite.conf")
        fp = node.get_content()
        return utils.read_config(fp)

    ## IAdminPanelProvider methods

    def get_admin_panels(self, req):
        if 'VERSIONCONTROL_ADMIN' in req.perm:
            yield ('versioncontrol', _('Version Control'), 'permissions',
                   _('Permissions'))

    def render_admin_panel(self, req, category, page, path_info):
        req.perm.require('VERSIONCONTROL_ADMIN')

        if req.method == 'POST':
            perms = {}
            for setting in req.args:
                try:
                    setting = json.loads(setting)
                except ValueError:
                    continue
                if not isinstance(
                        setting, dict
                ) or 'perm' not in setting or 'user' not in setting or 'repo' not in setting:
                    continue
                repo = setting['repo']
                perm = setting['perm']
                user = setting['user']
                if repo not in perms:
                    perms[repo] = {}
                if perm not in perms[repo]:
                    perms[repo][perm] = []
                if user not in perms[repo][perm]:
                    perms[repo][perm].append(user)

            gitolite_admin_perms = perms.get(self.gitolite_admin_real_reponame,
                                             {})
            if (self.gitolite_admin_system_user
                    not in gitolite_admin_perms.get('R', [])
                    or self.gitolite_admin_system_user
                    not in gitolite_admin_perms.get('W', [])):
                add_warning(
                    req,
                    _('Read and write permissions on the gitolite admin repo must not be revoked for user %s -- otherwise this plugin will no longer work!'
                      % self.gitolite_admin_system_user))
                req.redirect(req.href.admin(category, page))

            utils.save_file(self.gitolite_admin_ssh_path, 'conf/gitolite.conf',
                            utils.to_string(perms),
                            _('Updating repository permissions'))

            add_notice(req, _('The permissions have been updated.'))
            req.redirect(req.href.admin(category, page))

        perms = self.read_config()

        users_listed_in_perms = set()
        flattened_perms = set()

        for p in perms.values():
            for perm in p:
                flattened_perms.add(perm)
                users_listed_in_perms.update(p[perm])
        flattened_perms = list(flattened_perms)

        def sort_perms(perms):
            tail = []
            ## Ensure the + goes last
            if '+' in perms:
                perms.remove("+")
                tail.append("+")
            perms = sorted(perms)
            perms.extend(tail)
            return perms

        flattened_perms = sort_perms(flattened_perms)

        users = sorted(
            list(set(list(self.get_users()) + list(users_listed_in_perms))))
        data = {
            'repositories': perms,
            'permissions': flattened_perms,
            'users': users,
            'sort_perms': sort_perms
        }
        return 'admin_repository_permissions.html', data

    # ITemplateProvider methods

    def get_htdocs_dirs(self):
        return []

    def get_templates_dirs(self):
        return [pkg_resources.resource_filename('trac_gitolite', 'templates')]
コード例 #16
0
ファイル: authz_policy.py プロジェクト: ohanar/trac
class AuthzPolicy(Component):
    """Permission policy using an authz-like configuration file.

    Refer to SVN documentation for syntax of the authz file. Groups are
    supported.

    As the fine-grained permissions brought by this permission policy are
    often used in complement of the other pemission policies (like the
    `DefaultPermissionPolicy`), there's no need to redefine all the
    permissions here. Only additional rights or restrictions should be added.

    === Installation ===
    Note that this plugin requires the `configobj` package:

        http://www.voidspace.org.uk/python/configobj.html

    You should be able to install it by doing a simple `easy_install configobj`

    Enabling this policy requires listing it in `trac.ini:
    {{{
    [trac]
    permission_policies = AuthzPolicy, DefaultPermissionPolicy

    [authz_policy]
    authz_file = conf/authzpolicy.conf
    }}}

    This means that the `AuthzPolicy` permissions will be checked first, and
    only if no rule is found will the `DefaultPermissionPolicy` be used.


    === Configuration ===
    The `authzpolicy.conf` file is a `.ini` style configuration file.

     - Each section of the config is a glob pattern used to match against a
       Trac resource descriptor. These descriptors are in the form:
       {{{
       <realm>:<id>@<version>[/<realm>:<id>@<version> ...]
       }}}
       Resources are ordered left to right, from parent to child. If any
       component is inapplicable, `*` is substituted. If the version pattern is
       not specified explicitely, all versions (`@*`) is added implicitly

       Example: Match the WikiStart page
       {{{
       [wiki:*]
       [wiki:WikiStart*]
       [wiki:WikiStart@*]
       [wiki:WikiStart]
       }}}

       Example: Match the attachment `wiki:WikiStart@117/attachment/FOO.JPG@*`
       on WikiStart
       {{{
       [wiki:*]
       [wiki:WikiStart*]
       [wiki:WikiStart@*]
       [wiki:WikiStart@*/attachment/*]
       [wiki:WikiStart@117/attachment/FOO.JPG]
       }}}

     - Sections are checked against the current Trac resource '''IN ORDER''' of
       appearance in the configuration file. '''ORDER IS CRITICAL'''.

     - Once a section matches, the current username is matched, '''IN ORDER''',
       against the keys of the section. If a key is prefixed with a `@`, it is
       treated as a group. If a key is prefixed with a `!`, the permission is
       denied rather than granted. The username will match any of 'anonymous',
       'authenticated', <username> or '*', using normal Trac permission rules.

    Example configuration:
    {{{
    [groups]
    administrators = athomas

    [*/attachment:*]
    * = WIKI_VIEW, TICKET_VIEW

    [wiki:WikiStart@*]
    @administrators = WIKI_ADMIN
    anonymous = WIKI_VIEW
    * = WIKI_VIEW

    # Deny access to page templates
    [wiki:PageTemplates/*]
    * =

    # Match everything else
    [*]
    @administrators = TRAC_ADMIN
    anonymous = BROWSER_VIEW, CHANGESET_VIEW, FILE_VIEW, LOG_VIEW,
        MILESTONE_VIEW, POLL_VIEW, REPORT_SQL_VIEW, REPORT_VIEW, ROADMAP_VIEW,
        SEARCH_VIEW, TICKET_CREATE, TICKET_MODIFY, TICKET_VIEW, TIMELINE_VIEW,
        WIKI_CREATE, WIKI_MODIFY, WIKI_VIEW
    # Give authenticated users some extra permissions
    authenticated = REPO_SEARCH, XML_RPC
    }}}
    """
    implements(IPermissionPolicy)

    authz_file = Option('authz_policy', 'authz_file', '',
                        'Location of authz policy configuration file.')

    authz = None
    authz_mtime = None

    # IPermissionPolicy methods

    def check_permission(self, action, username, resource, perm):
        if ConfigObj is None:
            self.log.error('configobj package not found')
            return None

        if self.authz_file and not self.authz_mtime or \
                os.path.getmtime(self.get_authz_file()) > self.authz_mtime:
            self.parse_authz()
        resource_key = self.normalise_resource(resource)
        self.log.debug('Checking %s on %s', action, resource_key)
        permissions = self.authz_permissions(resource_key, username)
        if permissions is None:
            return None  # no match, can't decide
        elif permissions == ['']:
            return False  # all actions are denied

        # FIXME: expand all permissions once for all
        ps = PermissionSystem(self.env)
        for deny, perms in groupby(permissions,
                                   key=lambda p: p.startswith('!')):
            if deny and action in ps.expand_actions([p[1:] for p in perms]):
                return False  # action is explicitly denied
            elif action in ps.expand_actions(perms):
                return True  # action is explicitly granted

        return None  # no match for action, can't decide

    # Internal methods

    def get_authz_file(self):
        f = self.authz_file
        return f if os.path.isabs(f) else os.path.join(self.env.path, f)

    def parse_authz(self):
        self.log.debug('Parsing authz security policy %s',
                       self.get_authz_file())
        self.authz = ConfigObj(self.get_authz_file(), encoding='utf8')
        groups = {}
        for group, users in self.authz.get('groups', {}).iteritems():
            if isinstance(users, basestring):
                users = [users]
            groups[group] = users

        self.groups_by_user = {}

        def add_items(group, items):
            for item in items:
                if item.startswith('@'):
                    add_items(group, groups[item[1:]])
                else:
                    self.groups_by_user.setdefault(item, set()).add(group)

        for group, users in groups.iteritems():
            add_items('@' + group, users)

        self.authz_mtime = os.path.getmtime(self.get_authz_file())

    def normalise_resource(self, resource):
        def flatten(resource):
            if not resource:
                return ['*:*@*']
            if not (resource.realm or resource.id):
                return [
                    '%s:%s@%s' % (resource.realm or '*', resource.id
                                  or '*', resource.version or '*')
                ]
            # XXX Due to the mixed functionality in resource we can end up with
            # ticket, ticket:1, ticket:1@10. This code naively collapses all
            # subsets of the parent resource into one. eg. ticket:1@10
            parent = resource.parent
            while parent and (resource.realm == parent.realm or
                              (resource.realm == parent.realm
                               and resource.id == parent.id)):
                parent = parent.parent
            if parent:
                parent = flatten(parent)
            else:
                parent = []
            return parent + [
                '%s:%s@%s' % (resource.realm or '*', resource.id
                              or '*', resource.version or '*')
            ]

        return '/'.join(flatten(resource))

    def authz_permissions(self, resource_key, username):
        # TODO: Handle permission negation in sections. eg. "if in this
        # ticket, remove TICKET_MODIFY"
        if username and username != 'anonymous':
            valid_users = ['*', 'authenticated', username]
        else:
            valid_users = ['*', 'anonymous']
        for resource_section in [
                a for a in self.authz.sections if a != 'groups'
        ]:
            resource_glob = resource_section
            if '@' not in resource_glob:
                resource_glob += '@*'

            if fnmatch(resource_key, resource_glob):
                section = self.authz[resource_section]
                for who, permissions in section.iteritems():
                    if who in valid_users or \
                            who in self.groups_by_user.get(username, []):
                        self.log.debug('%s matched section %s for user %s',
                                       resource_key, resource_glob, username)
                        if isinstance(permissions, basestring):
                            return [permissions]
                        else:
                            return permissions
        return None
コード例 #17
0
ファイル: api.py プロジェクト: t2y/trac
class RepositoryManager(Component):
    """Version control system manager."""

    implements(IRequestFilter, IResourceManager, IRepositoryProvider)

    connectors = ExtensionPoint(IRepositoryConnector)
    providers = ExtensionPoint(IRepositoryProvider)
    change_listeners = ExtensionPoint(IRepositoryChangeListener)

    repositories_section = ConfigSection(
        'repositories',
        """One of the alternatives for registering new repositories is to
        populate the `[repositories]` section of the `trac.ini`.

        This is especially suited for setting up convenience aliases,
        short-lived repositories, or during the initial phases of an
        installation.

        See [TracRepositoryAdmin#Intrac.ini TracRepositoryAdmin] for details
        about the format adopted for this section and the rest of that page for
        the other alternatives.

        (''since 0.12'')""")

    repository_type = Option(
        'trac', 'repository_type', 'svn',
        """Default repository connector type. (''since 0.10'')

        This is also used as the default repository type for repositories
        defined in [[TracIni#repositories-section repositories]] or using the
        "Repositories" admin panel. (''since 0.12'')
        """)

    repository_dir = Option(
        'trac', 'repository_dir', '',
        """Path to the default repository. This can also be a relative path
        (''since 0.11'').

        This option is deprecated, and repositories should be defined in the
        [TracIni#repositories-section repositories] section, or using the
        "Repositories" admin panel. (''since 0.12'')""")

    repository_sync_per_request = ListOption(
        'trac',
        'repository_sync_per_request',
        '(default)',
        doc="""List of repositories that should be synchronized on every page
        request.

        Leave this option empty if you have set up post-commit hooks calling
        `trac-admin $ENV changeset added` on all your repositories
        (recommended). Otherwise, set it to a comma-separated list of
        repository names. Note that this will negatively affect performance,
        and will prevent changeset listeners from receiving events from the
        repositories specified here. (''since 0.12'')""")

    def __init__(self):
        self._cache = {}
        self._lock = threading.Lock()
        self._connectors = None
        self._all_repositories = None

    # IRequestFilter methods

    def pre_process_request(self, req, handler):
        from trac.web.chrome import Chrome, add_warning
        if handler is not Chrome(self.env):
            for reponame in self.repository_sync_per_request:
                start = time.time()
                if is_default(reponame):
                    reponame = ''
                try:
                    repo = self.get_repository(reponame)
                    if repo:
                        repo.sync()
                    else:
                        self.log.warning(
                            "Unable to find repository '%s' for "
                            "synchronization", reponame or '(default)')
                        continue
                except TracError as e:
                    add_warning(
                        req,
                        _(
                            "Can't synchronize with repository \"%(name)s\" "
                            "(%(error)s). Look in the Trac log for more "
                            "information.",
                            name=reponame or '(default)',
                            error=to_unicode(e)))
                except Exception as e:
                    add_warning(
                        req,
                        _(
                            "Failed to sync with repository \"%(name)s\": "
                            "%(error)s; repository information may be out of "
                            "date. Look in the Trac log for more information "
                            "including mitigation strategies.",
                            name=reponame or '(default)',
                            error=to_unicode(e)))
                    self.log.error(
                        "Failed to sync with repository \"%s\"; You may be "
                        "able to reduce the impact of this issue by "
                        "configuring [trac] repository_sync_per_request; see "
                        "http://trac.edgewall.org/wiki/TracRepositoryAdmin"
                        "#ExplicitSync for more detail: %s",
                        reponame or '(default)',
                        exception_to_unicode(e, traceback=True))
                self.log.info("Synchronized '%s' repository in %0.2f seconds",
                              reponame or '(default)',
                              time.time() - start)
        return handler

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

    # IResourceManager methods

    def get_resource_realms(self):
        yield 'changeset'
        yield 'source'
        yield 'repository'

    def get_resource_description(self, resource, format=None, **kwargs):
        if resource.realm == 'changeset':
            parent = resource.parent
            reponame = parent and parent.id
            id = resource.id
            if reponame:
                return _("Changeset %(rev)s in %(repo)s",
                         rev=id,
                         repo=reponame)
            else:
                return _("Changeset %(rev)s", rev=id)
        elif resource.realm == 'source':
            parent = resource.parent
            reponame = parent and parent.id
            id = resource.id
            version = ''
            if format == 'summary':
                repos = self.get_repository(reponame)
                node = repos.get_node(resource.id, resource.version)
                if node.isdir:
                    kind = _("directory")
                elif node.isfile:
                    kind = _("file")
                if resource.version:
                    version = _(" at version %(rev)s", rev=resource.version)
            else:
                kind = _("path")
                if resource.version:
                    version = '@%s' % resource.version
            in_repo = _(" in %(repo)s", repo=reponame) if reponame else ''
            # TRANSLATOR: file /path/to/file.py at version 13 in reponame
            return _('%(kind)s %(id)s%(at_version)s%(in_repo)s',
                     kind=kind,
                     id=id,
                     at_version=version,
                     in_repo=in_repo)
        elif resource.realm == 'repository':
            if not resource.id:
                return _("Default repository")
            return _("Repository %(repo)s", repo=resource.id)

    def get_resource_url(self, resource, href, **kwargs):
        if resource.realm == 'changeset':
            parent = resource.parent
            return href.changeset(resource.id, parent and parent.id or None)
        elif resource.realm == 'source':
            parent = resource.parent
            return href.browser(parent and parent.id or None,
                                resource.id,
                                rev=resource.version or None)
        elif resource.realm == 'repository':
            return href.browser(resource.id or None)

    def resource_exists(self, resource):
        if resource.realm == 'repository':
            reponame = resource.id
        else:
            reponame = resource.parent.id
        repos = self.env.get_repository(reponame)
        if not repos:
            return False
        if resource.realm == 'changeset':
            try:
                repos.get_changeset(resource.id)
                return True
            except NoSuchChangeset:
                return False
        elif resource.realm == 'source':
            try:
                repos.get_node(resource.id, resource.version)
                return True
            except NoSuchNode:
                return False
        elif resource.realm == 'repository':
            return True

    # IRepositoryProvider methods

    def get_repositories(self):
        """Retrieve repositories specified in TracIni.

        The `[repositories]` section can be used to specify a list
        of repositories.
        """
        repositories = self.repositories_section
        reponames = {}
        # eventually add pre-0.12 default repository
        if self.repository_dir:
            reponames[''] = {'dir': self.repository_dir}
        # first pass to gather the <name>.dir entries
        for option in repositories:
            if option.endswith('.dir'):
                reponames[option[:-4]] = {}
        # second pass to gather aliases
        for option in repositories:
            alias = repositories.get(option)
            if '.' not in option:  # Support <alias> = <repo> syntax
                option += '.alias'
            if option.endswith('.alias') and alias in reponames:
                reponames.setdefault(option[:-6], {})['alias'] = alias
        # third pass to gather the <name>.<detail> entries
        for option in repositories:
            if '.' in option:
                name, detail = option.rsplit('.', 1)
                if name in reponames and detail != 'alias':
                    reponames[name][detail] = repositories.get(option)

        for reponame, info in reponames.iteritems():
            yield (reponame, info)

    # Public API methods

    def get_supported_types(self):
        """Return the list of supported repository types."""
        types = set(type_ for connector in self.connectors
                    for (type_, prio) in connector.get_supported_types() or []
                    if prio >= 0)
        return list(types)

    def get_repositories_by_dir(self, directory):
        """Retrieve the repositories based on the given directory.

           :param directory: the key for identifying the repositories.
           :return: list of `Repository` instances.
        """
        directory = os.path.join(os.path.normcase(directory), '')
        repositories = []
        for reponame, repoinfo in self.get_all_repositories().iteritems():
            dir = repoinfo.get('dir')
            if dir:
                dir = os.path.join(os.path.normcase(dir), '')
                if dir.startswith(directory):
                    repos = self.get_repository(reponame)
                    if repos:
                        repositories.append(repos)
        return repositories

    def get_repository_id(self, reponame):
        """Return a unique id for the given repository name.

        This will create and save a new id if none is found.

        Note: this should probably be renamed as we're dealing
              exclusively with *db* repository ids here.
        """
        with self.env.db_transaction as db:
            for id, in db(
                    "SELECT id FROM repository WHERE name='name' AND value=%s",
                (reponame, )):
                return id

            id = db("SELECT COALESCE(MAX(id), 0) FROM repository")[0][0] + 1
            db("INSERT INTO repository (id, name, value) VALUES (%s, %s, %s)",
               (id, 'name', reponame))
            return id

    def get_repository(self, reponame):
        """Retrieve the appropriate `Repository` for the given
        repository name.

           :param reponame: the key for specifying the repository.
                            If no name is given, take the default
                            repository.
           :return: if no corresponding repository was defined,
                    simply return `None`.
        """
        reponame = reponame or ''
        repoinfo = self.get_all_repositories().get(reponame, {})
        if 'alias' in repoinfo:
            reponame = repoinfo['alias']
            repoinfo = self.get_all_repositories().get(reponame, {})
        rdir = repoinfo.get('dir')
        if not rdir:
            return None
        rtype = repoinfo.get('type') or self.repository_type

        # get a Repository for the reponame (use a thread-level cache)
        with self.env.db_transaction:  # prevent possible deadlock, see #4465
            with self._lock:
                tid = threading._get_ident()
                if tid in self._cache:
                    repositories = self._cache[tid]
                else:
                    repositories = self._cache[tid] = {}
                repos = repositories.get(reponame)
                if not repos:
                    if not os.path.isabs(rdir):
                        rdir = os.path.join(self.env.path, rdir)
                    connector = self._get_connector(rtype)
                    repos = connector.get_repository(rtype, rdir,
                                                     repoinfo.copy())
                    repositories[reponame] = repos
                return repos

    def get_repository_by_path(self, path):
        """Retrieve a matching `Repository` for the given `path`.

        :param path: the eventually scoped repository-scoped path
        :return: a `(reponame, repos, path)` triple, where `path` is
                 the remaining part of `path` once the `reponame` has
                 been truncated, if needed.
        """
        matches = []
        path = path.strip('/') + '/' if path else '/'
        for reponame in self.get_all_repositories().keys():
            stripped_reponame = reponame.strip('/') + '/'
            if path.startswith(stripped_reponame):
                matches.append((len(stripped_reponame), reponame))
        if matches:
            matches.sort()
            length, reponame = matches[-1]
            path = path[length:]
        else:
            reponame = ''
        return (reponame, self.get_repository(reponame), path.rstrip('/')
                or '/')

    def get_default_repository(self, context):
        """Recover the appropriate repository from the current context.

        Lookup the closest source or changeset resource in the context
        hierarchy and return the name of its associated repository.
        """
        while context:
            if context.resource.realm in ('source', 'changeset'):
                return context.resource.parent.id
            context = context.parent

    def get_all_repositories(self):
        """Return a dictionary of repository information, indexed by name."""
        if not self._all_repositories:
            all_repositories = {}
            for provider in self.providers:
                for reponame, info in provider.get_repositories() or []:
                    if reponame in all_repositories:
                        self.log.warn("Discarding duplicate repository '%s'",
                                      reponame)
                    else:
                        info['name'] = reponame
                        if 'id' not in info:
                            info['id'] = self.get_repository_id(reponame)
                        all_repositories[reponame] = info
            self._all_repositories = all_repositories
        return self._all_repositories

    def get_real_repositories(self):
        """Return a set of all real repositories (i.e. excluding aliases)."""
        repositories = set()
        for reponame in self.get_all_repositories():
            try:
                repos = self.get_repository(reponame)
                if repos is not None:
                    repositories.add(repos)
            except TracError:
                pass  # Skip invalid repositories
        return repositories

    def reload_repositories(self):
        """Reload the repositories from the providers."""
        with self._lock:
            # FIXME: trac-admin doesn't reload the environment
            self._cache = {}
            self._all_repositories = None
        self.config.touch()  # Force environment reload

    def notify(self, event, reponame, revs):
        """Notify repositories and change listeners about repository events.

        The supported events are the names of the methods defined in the
        `IRepositoryChangeListener` interface.
        """
        self.log.debug("Event %s on repository '%s' for changesets %r", event,
                       reponame or '(default)', revs)

        # Notify a repository by name, and all repositories with the same
        # base, or all repositories by base or by repository dir
        repos = self.get_repository(reponame)
        repositories = []
        if repos:
            base = repos.get_base()
        else:
            dir = os.path.abspath(reponame)
            repositories = self.get_repositories_by_dir(dir)
            if repositories:
                base = None
            else:
                base = reponame
        if base:
            repositories = [
                r for r in self.get_real_repositories() if r.get_base() == base
            ]
        if not repositories:
            self.log.warn("Found no repositories matching '%s' base.", base
                          or reponame)
            return
        for repos in sorted(repositories, key=lambda r: r.reponame):
            repos.sync()
            for rev in revs:
                args = []
                if event == 'changeset_modified':
                    args.append(repos.sync_changeset(rev))
                try:
                    changeset = repos.get_changeset(rev)
                except NoSuchChangeset:
                    try:
                        repos.sync_changeset(rev)
                        changeset = repos.get_changeset(rev)
                    except NoSuchChangeset:
                        self.log.debug(
                            "No changeset '%s' found in repository '%s'. "
                            "Skipping subscribers for event %s", rev,
                            repos.reponame or '(default)', event)
                        continue
                self.log.debug("Event %s on repository '%s' for revision '%s'",
                               event, repos.reponame or '(default)', rev)
                for listener in self.change_listeners:
                    getattr(listener, event)(repos, changeset, *args)

    def shutdown(self, tid=None):
        """Free `Repository` instances bound to a given thread identifier"""
        if tid:
            assert tid == threading._get_ident()
            with self._lock:
                repositories = self._cache.pop(tid, {})
                for reponame, repos in repositories.iteritems():
                    repos.close()

    # private methods

    def _get_connector(self, rtype):
        """Retrieve the appropriate connector for the given repository type.

        Note that the self._lock must be held when calling this method.
        """
        if self._connectors is None:
            # build an environment-level cache for the preferred connectors
            self._connectors = {}
            for connector in self.connectors:
                for type_, prio in connector.get_supported_types() or []:
                    keep = (connector, prio)
                    if type_ in self._connectors and \
                            prio <= self._connectors[type_][1]:
                        keep = None
                    if keep:
                        self._connectors[type_] = keep
        if rtype in self._connectors:
            connector, prio = self._connectors[rtype]
            if prio >= 0:  # no error condition
                return connector
            else:
                raise TracError(
                    _(
                        'Unsupported version control system "%(name)s"'
                        ': %(error)s',
                        name=rtype,
                        error=to_unicode(connector.error)))
        else:
            raise TracError(
                _(
                    'Unsupported version control system "%(name)s": '
                    'Can\'t find an appropriate component, maybe the '
                    'corresponding plugin was not enabled? ',
                    name=rtype))
コード例 #18
0
ファイル: main.py プロジェクト: pombredanne/trachacks
class TasklistPlugin(QueryModule):
    implements(ITemplateProvider, ITemplateStreamFilter)

    ticket_manipulators = ExtensionPoint(ITicketManipulator)

    field_name = Option('tasklist', 'tasklist_field', default='action_item')

    default_query = Option(
        'tasklist',
        'default_query',
        default='status!=closed&owner=$USER',
        doc='The default tasklist query for authenticated users.')

    default_anonymous_query = Option(
        'tasklist',
        'default_anonymous_query',
        default='status!=closed&cc~=$USER',
        doc='The default tasklist query for anonymous users.')

    default_cols = ListOption(
        'tasklist',
        'default_cols',
        default=['id', 'summary', 'priority'],
        doc='The default list of columns to show in the tasklist.')

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

    def get_navigation_items(self, req):
        if req.perm.has_permission('TICKET_VIEW'):
            yield ('mainnav', 'tasklist',
                   tag.a('Tasklist', href=req.href.tasklist()))

    # IRequestHandler methods
    def match_request(self, req):
        return req.path_info.startswith('/tasklist')

    def process_request(self, req):
        req.perm.assert_permission('TICKET_VIEW')

        if not self.env.config.has_option('ticket-custom', self.field_name):
            raise TracError(
                'Configuration error: the custom ticket field "%s" has not been defined '
                'in the [ticket-custom] section of trac.ini. See the documentation '
                'for more info on configuring the TaskListPlugin.' %
                self.field_name)

        constraints = self._get_constraints(req)
        if not constraints and not 'order' in req.args:
            # If no constraints are given in the URL, use the default ones.
            if req.authname and req.authname != 'anonymous':
                qstring = self.default_query
                user = req.authname
            else:
                email = req.session.get('email')
                name = req.session.get('name')
                qstring = self.default_anonymous_query
                user = email or name or None

            if user:
                qstring = qstring.replace('$USER', user)
            self.log.debug('TasklistPlugin: Using default query: %s', qstring)
            constraints = Query.from_string(self.env, qstring).constraints
            # Ensure no field constraints that depend on $USER are used
            # if we have no username.
            for field, vals in constraints.items():
                for val in vals:
                    if val.endswith('$USER'):
                        del constraints[field]

        cols = req.args.get('col')
        if not cols:
            cols = self.default_cols
            cols.append(self.field_name)

        if isinstance(cols, basestring):
            cols = [cols]
        form_cols = copy.copy(cols)
        # Since we don't show 'id' or the tasklist_field as an option
        # to the user, we need to re-insert it here.
        if cols and 'id' not in cols:
            cols.insert(0, 'id')
        if cols and self.field_name not in cols:
            cols.insert(0, self.field_name)

        rows = req.args.get('row', [])
        if isinstance(rows, basestring):
            rows = [rows]

        q = Query(self.env, constraints=constraints, cols=cols)
        query = Query(self.env, req.args.get('report'), constraints, cols,
                      req.args.get('order'), 'desc' in req.args,
                      req.args.get('group'), 'groupdesc' in req.args, 'verbose'
                      in req.args, rows, req.args.get('limit'))

        if 'update' in req.args:
            # Reset session vars
            for var in ('query_constraints', 'query_time', 'query_tickets'):
                if var in req.session:
                    del req.session[var]
            req.redirect(q.get_href(req.href).replace('/query', '/tasklist'))

        if 'add' in req.args:
            req.perm.require('TICKET_CREATE')
            t = Ticket(self.env)
            if req.method == 'POST' and 'field_owner' in req.args and \
                   'TICKET_MODIFY' not in req.perm:
                del req.args['field_owner']
            self._populate(req, t)
            reporter_id = req.args.get('field_reporter') or \
                          get_reporter_id(req, 'author')
            t.values['reporter'] = reporter_id
            valid = None
            valid = self._validate_ticket(req, t)
            if valid:
                t.insert()
                # Notify
                try:
                    tn = TicketNotifyEmail(self.env)
                    tn.notify(t, newticket=True)
                except Exception, e:
                    self.log.exception(
                        "Failure sending notification on creation of "
                        "ticket #%s: %s" % (t.id, e))
            req.redirect(q.get_href(req.href).replace('/query', '/tasklist'))

        template, data, mime_type = self.display_html(req, query)

        # We overlap the query session href var so that if a ticket is
        # entered from the tasklist the "Back to Query" link will
        # come back to the tasklist instead of the query module.
        query_href = req.session['query_href']
        req.session['query_href'] = query_href.replace('/query', '/tasklist')

        data['title'] = 'Task List'
        data['all_columns'].remove(self.field_name)
        #_pprint(data['tickets'])
        for ticket in data['tickets']:
            summary = ticket['summary']
            action = ticket[self.field_name]
            ticket['title'] = summary
            ticket['summary'] = action != '--' and action or summary
            continue
        for i, header in enumerate(data['headers']):
            header['href'] = header['href'].replace('/query', '/tasklist')
            if header['name'] == self.field_name:
                del_index = i
            continue
        del data['headers'][del_index]
        data['ticket_fields'] = self._get_ticket_fields(data)

        add_stylesheet(req, 'tasklist/css/tasklist.css')
        return 'tasklist.html', data, mime_type
コード例 #19
0
class MasterTicketsModule(Component):
    """Provides support for ticket dependencies."""

    implements(IRequestHandler, IRequestFilter, ITemplateStreamFilter,
               ITemplateProvider, ITicketManipulator)

    dot_path = Option('mastertickets',
                      'dot_path',
                      default='dot',
                      doc='Path to the dot executable.')
    gs_path = Option('mastertickets',
                     'gs_path',
                     default='gs',
                     doc='Path to the ghostscript executable.')
    use_gs = BoolOption(
        'mastertickets',
        'use_gs',
        default=False,
        doc='If enabled, use ghostscript to produce nicer output.')

    closed_color = Option('mastertickets',
                          'closed_color',
                          default='green',
                          doc='Color of closed tickets')
    opened_color = Option('mastertickets',
                          'opened_color',
                          default='red',
                          doc='Color of opened tickets')
    show_key = Option('mastertickets',
                      'show_key',
                      default=False,
                      doc='Show a key for open/closed nodes')
    closed_text = Option('mastertickets',
                         'closed_text',
                         default='Done',
                         doc='Text for key showing closed tickets')
    opened_text = Option('mastertickets',
                         'opened_text',
                         default='ToDo',
                         doc='Text for key showing opened tickets')
    highlight_target = Option('mastertickets',
                              'highlight_target',
                              default=False,
                              doc='Highlight target tickets in graph')
    full_graph = Option(
        'mastertickets',
        'full_graph',
        default=False,
        doc='Show full dep. graph, not just direct blocking links')

    graph_direction = ChoiceOption(
        'mastertickets',
        'graph_direction',
        choices=['TD', 'LR', 'DT', 'RL'],
        doc=
        'Direction of the dependency graph (TD = Top Down, DT = Down Top, LR = Left Right, RL = Right Left)'
    )

    fields = set(['blocking', 'blockedby'])

    # IRequestFilter methods
    def pre_process_request(self, req, handler):
        return handler

    def post_process_request(self, req, template, data, content_type):
        if req.path_info.startswith('/ticket/'):
            # In case of an invalid ticket, the data is invalid
            if not data:
                return template, data, content_type
            tkt = data['ticket']
            links = TicketLinks(self.env, tkt)

            for i in links.blocked_by:
                if Ticket(self.env, i)['status'] != 'closed':
                    add_script(req, 'mastertickets/disable_resolve.js')
                    break

            # Add link to depgraph if needed
            if links:
                add_ctxtnav(req, 'Depgraph', req.href.depgraph(tkt.id))

            for change in data.get('changes', {}):
                if not change.has_key('fields'):
                    continue
                for field, field_data in change['fields'].iteritems():
                    if field in self.fields:
                        if field_data['new'].strip():
                            new = set(
                                [int(n) for n in field_data['new'].split(',')])
                        else:
                            new = set()
                        if field_data['old'].strip():
                            old = set(
                                [int(n) for n in field_data['old'].split(',')])
                        else:
                            old = set()
                        add = new - old
                        sub = old - new
                        elms = tag()
                        if add:
                            elms.append(
                                tag.em(u', '.join(
                                    [unicode(n) for n in sorted(add)])))
                            elms.append(u' added')
                        if add and sub:
                            elms.append(u'; ')
                        if sub:
                            elms.append(
                                tag.em(u', '.join(
                                    [unicode(n) for n in sorted(sub)])))
                            elms.append(u' removed')
                        field_data['rendered'] = elms

        #add a link to generate a dependency graph for all the tickets in the milestone
        if req.path_info.startswith('/milestone/'):
            if not data:
                return template, data, content_type
            milestone = data['milestone']
            add_ctxtnav(req, 'Depgraph',
                        req.href.depgraph('milestone', milestone.name))

        return template, data, content_type

    # ITemplateStreamFilter methods
    def filter_stream(self, req, method, filename, stream, data):
        if not data:
            return stream

        # We try all at the same time to maybe catch also changed or processed templates
        if filename in [
                "report_view.html", "query_results.html", "ticket.html",
                "query.html"
        ]:
            # For ticket.html
            if 'fields' in data and isinstance(data['fields'], list):
                for field in data['fields']:
                    for f in self.fields:
                        if field['name'] == f and data['ticket'][f]:
                            field['rendered'] = self._link_tickets(
                                req, data['ticket'][f])
            # For query_results.html and query.html
            if 'groups' in data and isinstance(data['groups'], list):
                for group, tickets in data['groups']:
                    for ticket in tickets:
                        for f in self.fields:
                            if f in ticket:
                                ticket[f] = self._link_tickets(req, ticket[f])
            # For report_view.html
            if 'row_groups' in data and isinstance(data['row_groups'], list):
                for group, rows in data['row_groups']:
                    for row in rows:
                        if 'cell_groups' in row and isinstance(
                                row['cell_groups'], list):
                            for cells in row['cell_groups']:
                                for cell in cells:
                                    # If the user names column in the report differently (blockedby AS "blocked by") then this will not find it
                                    if cell.get('header',
                                                {}).get('col') in self.fields:
                                        cell['value'] = self._link_tickets(
                                            req, cell['value'])
        return stream

    # ITicketManipulator methods
    def prepare_ticket(self, req, ticket, fields, actions):
        pass

    def validate_ticket(self, req, ticket):
        if req.args.get('action') == 'resolve' and req.args.get(
                'action_resolve_resolve_resolution') == 'fixed':
            links = TicketLinks(self.env, ticket)
            for i in links.blocked_by:
                if Ticket(self.env, i)['status'] != 'closed':
                    yield None, 'Ticket #%s is blocking this ticket' % i

    # ITemplateProvider methods
    def get_htdocs_dirs(self):
        """Return the absolute path of a directory containing additional
        static resources (such as images, style sheets, etc).
        """
        return [('mastertickets', resource_filename(__name__, 'htdocs'))]

    def get_templates_dirs(self):
        """Return the absolute path of the directory containing the provided
        ClearSilver templates.
        """
        return [resource_filename(__name__, 'templates')]

    # IRequestHandler methods
    def match_request(self, req):
        return req.path_info.startswith('/depgraph')

    def process_request(self, req):
        path_info = req.path_info[10:]

        if not path_info:
            raise TracError('No ticket specified')

        #list of tickets to generate the depgraph for
        tkt_ids = []
        milestone = None
        split_path = path_info.split('/', 2)

        #Urls to generate the depgraph for a ticket is /depgraph/ticketnum
        #Urls to generate the depgraph for a milestone is /depgraph/milestone/milestone_name
        if split_path[0] == 'milestone':
            #we need to query the list of tickets in the milestone
            milestone = split_path[1]
            query = Query(self.env,
                          constraints={'milestone': [milestone]},
                          max=0)
            tkt_ids = [fields['id'] for fields in query.execute()]
        else:
            #the list is a single ticket
            tkt_ids = [int(split_path[0])]

        #the summary argument defines whether we place the ticket id or
        #it's summary in the node's label
        label_summary = 0
        if 'summary' in req.args:
            label_summary = int(req.args.get('summary'))

        g = self._build_graph(req, tkt_ids, label_summary=label_summary)
        if path_info.endswith('/depgraph.png') or 'format' in req.args:
            format = req.args.get('format')
            if format == 'text':
                #in case g.__str__ returns unicode, we need to convert it in ascii
                req.send(
                    to_unicode(g).encode('ascii', 'replace'), 'text/plain')
            elif format == 'debug':
                import pprint
                req.send(
                    pprint.pformat(
                        [TicketLinks(self.env, tkt_id) for tkt_id in tkt_ids]),
                    'text/plain')
            elif format is not None:
                req.send(g.render(self.dot_path, format), 'text/plain')

            if self.use_gs:
                ps = g.render(self.dot_path, 'ps2')
                gs = subprocess.Popen([
                    self.gs_path, '-q', '-dTextAlphaBits=4',
                    '-dGraphicsAlphaBits=4', '-sDEVICE=png16m',
                    '-sOutputFile=%stdout%', '-'
                ],
                                      stdin=subprocess.PIPE,
                                      stdout=subprocess.PIPE,
                                      stderr=subprocess.PIPE)
                img, err = gs.communicate(ps)
                if err:
                    self.log.debug('MasterTickets: Error from gs: %s', err)
            else:
                img = g.render(self.dot_path)
            req.send(img, 'image/png')
        else:
            data = {}

            #add a context link to enable/disable labels in nodes
            if label_summary:
                add_ctxtnav(req, 'Without labels',
                            req.href(req.path_info, summary=0))
            else:
                add_ctxtnav(req, 'With labels',
                            req.href(req.path_info, summary=1))

            if milestone is None:
                tkt = Ticket(self.env, tkt_ids[0])
                data['tkt'] = tkt
                add_ctxtnav(req, 'Back to Ticket #%s' % tkt.id,
                            req.href.ticket(tkt.id))
            else:
                add_ctxtnav(req, 'Back to Milestone %s' % milestone,
                            req.href.milestone(milestone))
            data['milestone'] = milestone
            data['graph'] = g
            data['graph_render'] = partial(g.render, self.dot_path)
            data['use_gs'] = self.use_gs

            return 'depgraph.html', data, None

    def _build_graph(self, req, tkt_ids, label_summary=0):
        g = graphviz.Graph()
        g.label_summary = label_summary

        g.attributes['rankdir'] = self.graph_direction

        node_default = g['node']
        node_default['style'] = 'filled'

        edge_default = g['edge']
        edge_default['style'] = ''

        # Force this to the top of the graph
        for id in tkt_ids:
            g[id]

        if self.show_key:
            g[-1]['label'] = self.closed_text
            g[-1]['fillcolor'] = self.closed_color
            g[-1]['shape'] = 'box'
            g[-2]['label'] = self.opened_text
            g[-2]['fillcolor'] = self.opened_color
            g[-2]['shape'] = 'box'

        links = TicketLinks.walk_tickets(self.env,
                                         tkt_ids,
                                         full=self.full_graph)
        links = sorted(links, key=lambda link: link.tkt.id)
        for link in links:
            tkt = link.tkt
            node = g[tkt.id]
            if label_summary:
                node['label'] = u'#%s %s' % (tkt.id, tkt['summary'])
            else:
                node['label'] = u'#%s' % tkt.id
            node['fillcolor'] = tkt[
                'status'] == 'closed' and self.closed_color or self.opened_color
            node['URL'] = req.href.ticket(tkt.id)
            node['alt'] = u'Ticket #%s' % tkt.id
            node['tooltip'] = tkt['summary']
            if self.highlight_target and tkt.id in tkt_ids:
                node['penwidth'] = 3

            for n in link.blocking:
                node > g[n]

        return g

    def _link_tickets(self, req, tickets):
        items = []

        for i, word in enumerate(re.split(r'([;,\s]+)', tickets)):
            if i % 2:
                items.append(word)
            elif word:
                ticketid = word
                word = '#%s' % word

                try:
                    ticket = Ticket(self.env, ticketid)
                    if 'TICKET_VIEW' in req.perm(ticket.resource):
                        word = \
                            tag.a(
                                '#%s' % ticket.id,
                                class_=ticket['status'],
                                href=req.href.ticket(int(ticket.id)),
                                title=shorten_line(ticket['summary'])
                            )
                except ResourceNotFound:
                    pass

                items.append(word)

        if items:
            return tag(items)
        else:
            return None
コード例 #20
0
class ODTExportPlugin(Component):
    """Convert Wiki pages to ODT."""
    implements(IContentConverter)

    img_width = Option('odtexport', 'img_default_width', '8cm')
    img_height = Option('odtexport', 'img_default_height', '6cm')
    img_dpi = IntOption('odtexport', 'dpi', '96')
    get_remote_images = BoolOption('odtexport', 'get_remote_images', True)
    replace_keyword = Option('odtexport', 'replace_keyword', 'TRAC-ODT-INSERT')
    wikiversion_keyword = Option('odtexport', 'wikiversion_keyword',
                                 'TRAC-ODT-WIKIVERSION')
    wikiname_keyword = Option('odtexport', 'wikiname_keyword',
                              'TRAC-ODT-WIKINAME')
    timestamp_keyword = Option('odtexport', 'timestamp_keyword',
                               'TRAC-ODT-TIMESTAMP')
    cut_start_keyword = Option('odtexport', 'cut_start_keyword',
                               'TRAC-ODT-CUT-START')
    cut_stop_keyword = Option('odtexport', 'cut_stop_keyword',
                              'TRAC-ODT-CUT-STOP')
    remove_macros = ListOption(
        'odtexport', 'remove_macros',
        "PageOutline, TracGuideToc, TOC, TranslatedPages")

    # IContentConverter methods
    def get_supported_conversions(self):
        yield ('odt', 'OpenDocument', 'odt', 'text/x-trac-wiki',
               'application/vnd.oasis.opendocument.text', 5)

    def convert_content(self, req, input_type, content,
                        output_type):  # pylint: disable-msg=W0613
        self.page_name = req.args.get('page', 'WikiStart')
        #wikipage = WikiPage(self.env, self.page_name)
        template = self.get_template_name(content)
        html = self.wiki_to_html(content, req)
        #return (html, "text/plain")
        odtfile = ODTFile(
            self.page_name,
            req.args.get('version', 'latest'),
            template,
            self.env,  # pylint: disable-msg=E1101
            options={
                "img_width": self.img_width,
                "img_height": self.img_height,
                "img_dpi": self.img_dpi,
                "get_remote_images": self.get_remote_images,
                "replace_keyword": self.replace_keyword,
                "wikiversion_keyword": self.wikiversion_keyword,
                "wikiname_keyword": self.wikiname_keyword,
                "timestamp_keyword": self.timestamp_keyword,
                "cut_start_keyword": self.cut_start_keyword,
                "cut_stop_keyword": self.cut_stop_keyword,
            })
        odtfile.open()
        #return (odtfile.import_xhtml(html), "text/plain")
        odtfile.import_xhtml(html)
        newdoc = odtfile.save()
        return (newdoc, "application/vnd.oasis.opendocument.text")

    def get_template_name(self, wikitext):
        template_macro = re.search('\[\[OdtTemplate\(([^)]+)\)\]\]', wikitext)
        if template_macro:
            tpl = template_macro.group(1)
            if tpl.endswith(".odt"):
                return tpl
            else:
                return "%s.odt" % tpl
        return "wikipage.odt"

    def wiki_to_html(self, wikitext, req):
        self.env.log.debug(
            'start function wiki_to_html')  # pylint: disable-msg=E1101

        # Remove some macros (TOC is better handled in ODT itself)
        for macro in self.remove_macros:
            wikitext = re.sub('\[\[%s(\([^)]*\))?\]\]' % macro, "", wikitext)

        # Now convert wiki to HTML
        out = StringIO()
        context = Context.from_request(req, absurls=True)
        Formatter(
            self.env,  # pylint: disable-msg=E1101
            context('wiki', self.page_name)).format(wikitext, out)
        html = Markup(out.getvalue())
        html = html.encode("utf-8", 'replace')

        # Clean up the HTML
        html = re.sub('<span class="icon">.</span>', '',
                      html)  # Remove external link icon
        tidy_options = dict(output_xhtml=1,
                            add_xml_decl=1,
                            indent=1,
                            tidy_mark=0,
                            input_encoding='utf8',
                            output_encoding='utf8',
                            doctype='auto',
                            wrap=0,
                            char_encoding='utf8')
        html = tidy.parseString(html, **tidy_options)
        # Replace nbsp with entity:
        # http://www.mail-archive.com/[email protected]/msg03670.html
        html = str(html).replace("&nbsp;", "&#160;")
        # Tidy creates newlines after <pre> (by indenting)
        html = re.sub('<pre([^>]*)>\n', '<pre\\1>', html)
        return html
コード例 #21
0
ファイル: filter.py プロジェクト: pombredanne/trachacks
class DateFieldModule(Component):
    """A module providing a JS date picker for custom fields."""

    date_format = Option(
        'datefield',
        'format',
        default='dmy',
        doc='The format to use for dates. Valid values are dmy, mdy, and ymd.')
    date_sep = Option('datefield',
                      'separator',
                      default='/',
                      doc='The separator character to use for dates.')

    implements(IRequestFilter, IRequestHandler, ITemplateProvider,
               ITicketManipulator)

    # IRequestHandler methods
    def match_request(self, req):
        return req.path_info.startswith('/datefield')

    def process_request(self, req):
        req.hdf['datefield.ids'] = list(self._date_fields())
        req.hdf['datefield.calendar'] = req.href.chrome(
            'datefield', 'calendar.png')
        req.hdf['datefield.format'] = self.date_format
        req.hdf['datefield.sep'] = self.date_sep

        return 'datefield.cs', 'text/javascript'

    # IRequestFilter methods
    def pre_process_request(self, req, handler):
        return handler

    def post_process_request(self, req, template, content_type):
        if req.path_info.startswith('/newticket') or req.path_info.startswith(
                '/ticket'):
            add_script(req, 'datefield/jquery.pack.js')
            add_script(req, 'datefield/jquery.datePicker.js')
            add_stylesheet(req, 'datefield/datePicker.css')

            # Add my dynamic JS junk
            idx = 0
            while req.hdf.get('chrome.scripts.%i.href' % idx):
                idx += 1
            req.hdf['chrome.scripts.%s' % idx] = {
                'href': req.href.datefield('datefield.js'),
                'type': 'text/javascript'
            }
        return template, content_type

    # ITemplateProvider methods
    def get_htdocs_dirs(self):
        from pkg_resources import resource_filename
        return [('datefield', resource_filename(__name__, 'htdocs'))]

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

    # ITicketManipulator methods
    def prepare_ticket(self, req, ticket, fields, actions):
        pass

    def validate_ticket(self, req, ticket):  # dmy mdy ymd
        for field in self._date_fields():
            try:
                val = ticket[field].strip()

                if not val and self.config['ticket-custom'].getbool(
                        field + '.date_empty', default=False):
                    continue
                if len(val.split(self.date_sep)) != 3:
                    raise Exception  # Token exception to force failure

                format = self.date_sep.join(
                    ['%' + c for c in self.date_format])
                try:
                    time.strptime(val, format)
                except ValueError:
                    time.strptime(val, format.replace('y', 'Y'))
            except Exception:
                self.log.debug(
                    'DateFieldModule: Got an exception, assuming it is a validation failure.\n'
                    + traceback.format_exc())
                yield field, 'Field %s does not seem to look like a date. The correct format is %s.' % \
                             (field, self.date_sep.join([c.upper()*(c=='y' and 4 or 2) for c in self.date_format]))

    # Internal methods
    def _date_fields(self):
        # XXX: Will this work when there is no ticket-custom section? <NPK>
        for key, value in self.config['ticket-custom'].options():
            if key.endswith('.date'):
                yield key.split('.', 1)[0]
コード例 #22
0
class PygmentsRenderer(Component):
    """HTML renderer for syntax highlighting based on Pygments."""

    implements(ISystemInfoProvider, IHTMLPreviewRenderer,
               IPreferencePanelProvider, IRequestHandler, ITemplateProvider)

    is_valid_default_handler = False

    pygments_lexer_options = ConfigSection(
        'pygments-lexer',
        """Configure Pygments [%(url)s lexer] options.

        For example, to set the
        [%(url)s#lexers-for-php-and-related-languages PhpLexer] options
        `startinline` and `funcnamehighlighting`:
        {{{#!ini
        [pygments-lexer]
        php.startinline = True
        php.funcnamehighlighting = True
        }}}

        The lexer name is derived from the class name, with `Lexer` stripped
        from the end. The lexer //short names// can also be used in place
        of the lexer name.
        """,
        doc_args={'url': 'http://pygments.org/docs/lexers/'})

    default_style = Option(
        'mimeviewer', 'pygments_default_style', 'trac',
        """The default style to use for Pygments syntax highlighting.""")

    pygments_modes = ListOption(
        'mimeviewer',
        'pygments_modes',
        '',
        doc="""List of additional MIME types known by Pygments.

        For each, a tuple `mimetype:mode:quality` has to be
        specified, where `mimetype` is the MIME type,
        `mode` is the corresponding Pygments mode to be used
        for the conversion and `quality` is the quality ratio
        associated to this conversion. That can also be used
        to override the default quality ratio used by the
        Pygments render.""")

    expand_tabs = True
    returns_source = True

    QUALITY_RATIO = 7

    EXAMPLE = """<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Hello, world!</title>
    <script>
      jQuery(document).ready(function($) {
        $("h1").fadeIn("slow");
      });
    </script>
  </head>
  <body>
    <h1>Hello, world!</h1>
  </body>
</html>"""

    # ISystemInfoProvider methods

    def get_system_info(self):
        version = get_pkginfo(pygments).get('version')
        # if installed from source, fallback to the hardcoded version info
        if not version and hasattr(pygments, '__version__'):
            version = pygments.__version__
        yield 'Pygments', version

    # IHTMLPreviewRenderer methods

    def get_extra_mimetypes(self):
        for _, aliases, _, mimetypes in get_all_lexers():
            for mimetype in mimetypes:
                yield mimetype, aliases

    def get_quality_ratio(self, mimetype):
        # Extend default MIME type to mode mappings with configured ones
        try:
            return self._types[mimetype][1]
        except KeyError:
            return 0

    def render(self, context, mimetype, content, filename=None, rev=None):
        req = context.req
        style = req.session.get('pygments_style', self.default_style)
        add_stylesheet(req, '/pygments/%s.css' % style)
        try:
            if len(content) > 0:
                mimetype = mimetype.split(';', 1)[0]
                language = self._types[mimetype][0]
                return self._generate(language, content, context)
        except (KeyError, ValueError):
            raise Exception("No Pygments lexer found for mime-type '%s'." %
                            mimetype)

    # IPreferencePanelProvider methods

    def get_preference_panels(self, req):
        yield 'pygments', _('Syntax Highlighting')

    def render_preference_panel(self, req, panel):
        styles = list(get_all_styles())

        if req.method == 'POST':
            style = req.args.get('style')
            if style and style in styles:
                req.session['pygments_style'] = style
                add_notice(req, _("Your preferences have been saved."))
            req.redirect(req.href.prefs(panel or None))

        for style in sorted(styles):
            add_stylesheet(req,
                           '/pygments/%s.css' % style,
                           title=style.title())
        output = self._generate('html', self.EXAMPLE)
        return 'prefs_pygments.html', {
            'output': output,
            'selection': req.session.get('pygments_style', self.default_style),
            'styles': styles
        }

    # IRequestHandler methods

    def match_request(self, req):
        match = re.match(r'/pygments/([-\w]+)\.css', req.path_info)
        if match:
            req.args['style'] = match.group(1)
            return True

    def process_request(self, req):
        style = req.args['style']
        try:
            style_cls = get_style_by_name(style)
        except ValueError as e:
            raise HTTPNotFound(e)

        parts = style_cls.__module__.split('.')
        filename = resource_filename('.'.join(parts[:-1]), parts[-1] + '.py')
        mtime = datetime.fromtimestamp(os.path.getmtime(filename), localtz)
        last_modified = http_date(mtime)
        if last_modified == req.get_header('If-Modified-Since'):
            req.send_response(304)
            req.end_headers()
            return

        formatter = HtmlFormatter(style=style_cls)
        content = u'\n\n'.join([
            formatter.get_style_defs('div.code pre'),
            formatter.get_style_defs('table.code td')
        ]).encode('utf-8')

        req.send_response(200)
        req.send_header('Content-Type', 'text/css; charset=utf-8')
        req.send_header('Last-Modified', last_modified)
        req.send_header('Content-Length', len(content))
        req.write(content)

    # ITemplateProvider methods

    def get_htdocs_dirs(self):
        return []

    def get_templates_dirs(self):
        return [resource_filename('trac.mimeview', 'templates')]

    # Internal methods

    @lazy
    def _lexer_alias_name_map(self):
        lexer_alias_name_map = {}
        for lexer_name, aliases, _, _ in get_all_lexers():
            name = aliases[0] if aliases else lexer_name
            for alias in aliases:
                lexer_alias_name_map[alias] = name
        return lexer_alias_name_map

    @lazy
    def _lexer_options(self):
        lexer_options = {}
        for key, lexer_option_value in self.pygments_lexer_options.options():
            try:
                lexer_name_or_alias, lexer_option_name = key.split('.')
            except ValueError:
                pass
            else:
                lexer_name = self._lexer_alias_to_name(lexer_name_or_alias)
                lexer_option = {lexer_option_name: lexer_option_value}
                lexer_options.setdefault(lexer_name, {}).update(lexer_option)
        return lexer_options

    @lazy
    def _types(self):
        types = {}
        for lexer_name, aliases, _, mimetypes in get_all_lexers():
            name = aliases[0] if aliases else lexer_name
            for mimetype in mimetypes:
                types[mimetype] = (name, self.QUALITY_RATIO)

        # Pygments < 1.4 doesn't know application/javascript
        if 'application/javascript' not in types:
            js_entry = types.get('text/javascript')
            if js_entry:
                types['application/javascript'] = js_entry

        types.update(Mimeview(self.env).configured_modes_mapping('pygments'))
        return types

    def _generate(self, language, content, context=None):
        lexer_name = self._lexer_alias_to_name(language)
        lexer_options = {'stripnl': False}
        lexer_options.update(self._lexer_options.get(lexer_name, {}))
        if context:
            lexer_options.update(context.get_hint('lexer_options', {}))
        lexer = get_lexer_by_name(lexer_name, **lexer_options)
        return GenshiHtmlFormatter().generate(lexer.get_tokens(content))

    def _lexer_alias_to_name(self, alias):
        return self._lexer_alias_name_map.get(alias, alias)
コード例 #23
0
ファイル: web_ui.py プロジェクト: rwbaumg/tractagsplugin
class TagRequestHandler(TagTemplateProvider):
    """[main] Implements the /tags handler."""

    implements(INavigationContributor, IRequestHandler)

    cloud_mincount = Option(
        'tags',
        'cloud_mincount',
        1,
        doc="""Integer threshold to hide tags with smaller count.""")
    default_cols = Option(
        'tags',
        'default_table_cols',
        'id|description|tags',
        doc="""Select columns and order for table format using a "|"-separated
            list of column names.

            Supported columns: realm, id, description, tags
            """)
    default_format = Option(
        'tags',
        'default_format',
        'oldlist',
        doc="""Set the default format for the handler of the `/tags` domain.

            || `oldlist` (default value) || The original format with a
            bulleted-list of "linked-id description (tags)" ||
            || `compact` || bulleted-list of "linked-description" ||
            || `table` || table... (see corresponding column option) ||
            """)
    exclude_realms = ListOption(
        'tags',
        'exclude_realms', [],
        doc="""Comma-separated list of realms to exclude from tags queries
            by default, unless specifically included using "realm:realm-name"
            in a query.""")

    # INavigationContributor methods
    def get_active_navigation_item(self, req):
        if 'TAGS_VIEW' in req.perm:
            return 'tags'

    def get_navigation_items(self, req):
        if 'TAGS_VIEW' in req.perm:
            label = tag_("Tags")
            yield ('mainnav', 'tags',
                   builder.a(label, href=req.href.tags(), accesskey='T'))

    # IRequestHandler methods
    def match_request(self, req):
        return req.path_info.startswith('/tags')

    def process_request(self, req):
        req.perm.require('TAGS_VIEW')

        match = re.match(r'/tags/?(.*)', req.path_info)
        tag_id = match.group(1) and match.group(1) or None
        query = req.args.get('q', '')

        # Consider only providers, that are permitted for display.
        tag_system = TagSystem(self.env)
        all_realms = tag_system.get_taggable_realms(req.perm)
        if not (tag_id or query) or [r for r in all_realms if r in req.args
                                     ] == []:
            for realm in all_realms:
                if realm not in self.exclude_realms:
                    req.args[realm] = 'on'
        checked_realms = [r for r in all_realms if r in req.args]
        if query:
            # Add permitted realms from query expression.
            checked_realms.extend(query_realms(query, all_realms))
        realm_args = dict(
            zip([r for r in checked_realms], ['on' for r in checked_realms]))
        # Switch between single tag and tag query expression mode.
        if tag_id and not re.match(r"""(['"]?)(\S+)\1$""", tag_id, re.UNICODE):
            # Convert complex, invalid tag ID's --> query expression.
            req.redirect(req.href.tags(realm_args, q=tag_id))
        elif query:
            single_page = re.match(r"""(['"]?)(\S+)\1$""", query, re.UNICODE)
            if single_page:
                # Convert simple query --> single tag.
                req.redirect(req.href.tags(single_page.group(2), realm_args))

        data = dict(page_title=_("Tags"), checked_realms=checked_realms)
        # Populate the TagsQuery form field.
        data['tag_query'] = tag_id and tag_id or query
        data['tag_realms'] = list(
            dict(name=realm, checked=realm in checked_realms)
            for realm in all_realms)
        if tag_id:
            data['tag_page'] = WikiPage(self.env,
                                        tag_system.wiki_page_prefix + tag_id)
        if query or tag_id:
            macro = 'ListTagged'
            # TRANSLATOR: The meta-nav link label.
            add_ctxtnav(req, _("Back to Cloud"), req.href.tags())
            args = "%s,format=%s,cols=%s" % \
                   (tag_id and tag_id or query, self.default_format,
                    self.default_cols)
            data['mincount'] = None
        else:
            macro = 'TagCloud'
            mincount = as_int(req.args.get('mincount', None),
                              self.cloud_mincount)
            args = mincount and "mincount=%s" % mincount or None
            data['mincount'] = mincount
        formatter = Formatter(self.env, web_context(req, Resource('tag')))
        self.env.log.debug("%s macro arguments: %s", macro, args and args
                           or '(none)')
        macros = TagWikiMacros(self.env)
        try:
            # Query string without realm throws 'NotImplementedError'.
            data['tag_body'] = checked_realms and \
                               macros.expand_macro(formatter, macro, args,
                                                   realms=checked_realms) \
                               or ''
        except InvalidQuery, e:
            data['tag_query_error'] = to_unicode(e)
            data['tag_body'] = macros.expand_macro(formatter, 'TagCloud', '')
        add_stylesheet(req, 'tags/css/tractags.css')
        return 'tag_view.html', data, None
コード例 #24
0
class BrowserModule(Component):

    implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
               IWikiSyntaxProvider, IHTMLPreviewAnnotator,
               IWikiMacroProvider)

    property_renderers = ExtensionPoint(IPropertyRenderer)

    realm = RepositoryManager.source_realm

    downloadable_paths = ListOption('browser', 'downloadable_paths',
                                    '/trunk, /branches/*, /tags/*',
        doc="""List of repository paths that can be downloaded.

        Leave this option empty if you want to disable all downloads, otherwise
        set it to a comma-separated list of authorized paths (those paths are
        glob patterns, i.e. "*" can be used as a wild card). In a
        multi-repository environment, the path must be qualified with the
        repository name if the path does not point to the default repository
        (e.g. /reponame/trunk). Note that a simple prefix matching is
        performed on the paths, so aliases won't get automatically resolved.
        """)

    color_scale = BoolOption('browser', 'color_scale', True,
        doc="""Enable colorization of the ''age'' column.

        This uses the same color scale as the source code annotation:
        blue is older, red is newer.
        """)

    NEWEST_COLOR = (255, 136, 136)

    newest_color = Option('browser', 'newest_color', repr(NEWEST_COLOR),
        doc="""(r,g,b) color triple to use for the color corresponding
        to the newest color, for the color scale used in ''blame'' or
        the browser ''age'' column if `color_scale` is enabled.
        """)

    OLDEST_COLOR = (136, 136, 255)

    oldest_color = Option('browser', 'oldest_color', repr(OLDEST_COLOR),
        doc="""(r,g,b) color triple to use for the color corresponding
        to the oldest color, for the color scale used in ''blame'' or
        the browser ''age'' column if `color_scale` is enabled.
        """)

    intermediate_point = Option('browser', 'intermediate_point', '',
        doc="""If set to a value between 0 and 1 (exclusive), this will be the
        point chosen to set the `intermediate_color` for interpolating
        the color value.
        """)

    intermediate_color = Option('browser', 'intermediate_color', '',
        doc="""(r,g,b) color triple to use for the color corresponding
        to the intermediate color, if two linear interpolations are used
        for the color scale (see `intermediate_point`).
        If not set, the intermediate color between `oldest_color` and
        `newest_color` will be used.
        """)

    render_unsafe_content = BoolOption('browser', 'render_unsafe_content',
                                        'false',
        """Whether raw files should be rendered in the browser, or only made
        downloadable.

        Pretty much any file may be interpreted as HTML by the browser,
        which allows a malicious user to create a file containing cross-site
        scripting attacks.

        For open repositories where anyone can check-in a file, it is
        recommended to leave this option disabled.""")

    hidden_properties = ListOption('browser', 'hide_properties', 'svk:merge',
        doc="""Comma-separated list of version control properties to hide from
        the repository browser.
        """)

    # public methods

    def get_custom_colorizer(self):
        """Returns a converter for values from [0.0, 1.0] to a RGB triple."""

        def interpolate(old, new, value):
            # Provides a linearly interpolated color triple for `value`
            # which must be a floating point value between 0.0 and 1.0
            return tuple([int(b + (a - b) * value) for a, b in zip(new, old)])

        def parse_color(rgb, default):
            # Get three ints out of a `rgb` string or return `default`
            try:
                t = tuple([int(v) for v in re.split(r'(\d+)', rgb)[1::2]])
                return t if len(t) == 3 else default
            except ValueError:
                return default

        newest_color = parse_color(self.newest_color, self.NEWEST_COLOR)
        oldest_color = parse_color(self.oldest_color, self.OLDEST_COLOR)
        try:
            intermediate = float(self.intermediate_point)
        except ValueError:
            intermediate = None
        if intermediate:
            intermediate_color = parse_color(self.intermediate_color, None)
            if not intermediate_color:
                intermediate_color = tuple([(a + b) / 2 for a, b in
                                            zip(newest_color, oldest_color)])
            def colorizer(value):
                if value <= intermediate:
                    value = value / intermediate
                    return interpolate(oldest_color, intermediate_color, value)
                else:
                    value = (value - intermediate) / (1.0 - intermediate)
                    return interpolate(intermediate_color, newest_color, value)
        else:
            def colorizer(value):
                return interpolate(oldest_color, newest_color, value)
        return colorizer

    # INavigationContributor methods

    def get_active_navigation_item(self, req):
        return 'browser'

    def get_navigation_items(self, req):
        rm = RepositoryManager(self.env)
        if any(repos.is_viewable(req.perm) for repos
                                           in rm.get_real_repositories()):
            yield ('mainnav', 'browser',
                   tag.a(_('Browse Source'), href=req.href.browser()))

    # IPermissionRequestor methods

    def get_permission_actions(self):
        return ['BROWSER_VIEW', 'FILE_VIEW']

    # IRequestHandler methods

    def match_request(self, req):
        match = re.match(r'/(export|browser|file)(/.*)?$', req.path_info)
        if match:
            mode, path = match.groups()
            if mode == 'export':
                if path and '/' in path:
                    path_elts = path.split('/', 2)
                    if len(path_elts) != 3:
                        return False
                    path = path_elts[2]
                    req.args['rev'] = path_elts[1]
                    req.args['format'] = 'raw'
            elif mode == 'file':
                req.redirect(req.href.browser(path, rev=req.args.get('rev'),
                                              format=req.args.get('format')),
                             permanent=True)
            req.args['path'] = path or '/'
            return True

    def process_request(self, req):
        presel = req.args.get('preselected')
        if presel and (presel + '/').startswith(req.href.browser() + '/'):
            req.redirect(presel)

        path = req.args.get('path', '/')
        rev = req.args.get('rev', '')
        if rev.lower() in ('', 'head'):
            rev = None
        format = req.args.get('format')
        order = req.args.get('order', 'name').lower()
        desc = 'desc' in req.args

        rm = RepositoryManager(self.env)
        all_repositories = rm.get_all_repositories()
        reponame, repos, path = rm.get_repository_by_path(path)

        # Repository index
        show_index = not reponame and path == '/'
        if show_index:
            if repos and (as_bool(all_repositories[''].get('hidden'))
                          or not repos.is_viewable(req.perm)):
                repos = None

        if not repos and reponame:
            raise ResourceNotFound(_("Repository '%(repo)s' not found",
                                     repo=reponame))

        if reponame and reponame != repos.reponame: # Redirect alias
            qs = req.query_string
            req.redirect(req.href.browser(repos.reponame or None, path)
                         + ('?' + qs if qs else ''))
        reponame = repos.reponame if repos else None

        # Find node for the requested path/rev
        context = web_context(req)
        node = None
        changeset = None
        display_rev = lambda rev: rev
        if repos:
            try:
                if rev:
                    rev = repos.normalize_rev(rev)
                # If `rev` is `None`, we'll try to reuse `None` consistently,
                # as a special shortcut to the latest revision.
                rev_or_latest = rev or repos.youngest_rev
                node = get_existing_node(req, repos, path, rev_or_latest)
            except NoSuchChangeset as e:
                raise ResourceNotFound(e, _('Invalid changeset number'))
            if node:
                try:
                    # use changeset instance to retrieve branches and tags
                    changeset = repos.get_changeset(node.rev)
                except NoSuchChangeset:
                    pass

            context = context.child(repos.resource.child(self.realm, path,
                                                   version=rev_or_latest))
            display_rev = repos.display_rev

        # Prepare template data
        path_links = get_path_links(req.href, reponame, path, rev,
                                    order, desc)

        repo_data = dir_data = file_data = None
        if show_index:
            repo_data = self._render_repository_index(
                                        context, all_repositories, order, desc)
        if node:
            if not node.is_viewable(req.perm):
                raise PermissionError('BROWSER_VIEW' if node.isdir else
                                      'FILE_VIEW', node.resource, self.env)
            if node.isdir:
                if format in ('zip',): # extension point here...
                    self._render_zip(req, context, repos, node, rev)
                    # not reached
                dir_data = self._render_dir(req, repos, node, rev, order, desc)
            elif node.isfile:
                file_data = self._render_file(req, context, repos, node, rev)

        if not repos and not (repo_data and repo_data['repositories']):
            # If no viewable repositories, check permission instead of
            # repos.is_viewable()
            req.perm.require('BROWSER_VIEW')
            if show_index:
                raise ResourceNotFound(_("No viewable repositories"))
            else:
                raise ResourceNotFound(_("No node %(path)s", path=path))

        quickjump_data = properties_data = None
        if node and not req.is_xhr:
            properties_data = self.render_properties(
                    'browser', context, node.get_properties())
            quickjump_data = list(repos.get_quickjump_entries(rev))

        data = {
            'context': context, 'reponame': reponame, 'repos': repos,
            'repoinfo': all_repositories.get(reponame or ''),
            'path': path, 'rev': node and node.rev, 'stickyrev': rev,
            'display_rev': display_rev, 'changeset': changeset,
            'created_path': node and node.created_path,
            'created_rev': node and node.created_rev,
            'properties': properties_data,
            'path_links': path_links,
            'order': order, 'desc': 1 if desc else None,
            'repo': repo_data, 'dir': dir_data, 'file': file_data,
            'quickjump_entries': quickjump_data,
            'wiki_format_messages':
                self.config['changeset'].getbool('wiki_format_messages'),
        }
        if req.is_xhr: # render and return the content only
            return 'dir_entries.html', data

        if dir_data or repo_data:
            add_script(req, 'common/js/expand_dir.js')
            add_script(req, 'common/js/keyboard_nav.js')

        # Links for contextual navigation
        if node:
            if node.isfile:
                prev_rev = repos.previous_rev(rev=node.created_rev,
                                              path=node.created_path)
                if prev_rev:
                    href = req.href.browser(reponame,
                                            node.created_path, rev=prev_rev)
                    add_link(req, 'prev', href,
                             _('Revision %(num)s', num=display_rev(prev_rev)))
                if rev is not None:
                    add_link(req, 'up', req.href.browser(reponame,
                                                         node.created_path))
                next_rev = repos.next_rev(rev=node.created_rev,
                                          path=node.created_path)
                if next_rev:
                    href = req.href.browser(reponame, node.created_path,
                                            rev=next_rev)
                    add_link(req, 'next', href,
                             _('Revision %(num)s', num=display_rev(next_rev)))
                prevnext_nav(req, _('Previous Revision'), _('Next Revision'),
                             _('Latest Revision'))
            else:
                if path != '/':
                    add_link(req, 'up', path_links[-2]['href'],
                             _('Parent directory'))
                add_ctxtnav(req, tag.a(_('Last Change'),
                            href=req.href.changeset(node.created_rev, reponame,
                                                    node.created_path)))
            if node.isfile:
                annotate = data['file']['annotate']
                if annotate:
                    add_ctxtnav(req, _('Normal'),
                                title=_('View file without annotations'),
                                href=req.href.browser(reponame,
                                                      node.created_path,
                                                      rev=rev))
                if annotate != 'blame':
                    add_ctxtnav(req, _('Blame'),
                                title=_('Annotate each line with the last '
                                        'changed revision '
                                        '(this can be time consuming...)'),
                                href=req.href.browser(reponame,
                                                      node.created_path,
                                                      rev=rev,
                                                      annotate='blame'))
            add_ctxtnav(req, _('Revision Log'),
                        href=req.href.log(reponame, path, rev=rev))
            path_url = repos.get_path_url(path, rev)
            if path_url:
                if path_url.startswith('//'):
                    path_url = req.scheme + ':' + path_url
                add_ctxtnav(req, _('Repository URL'), href=path_url)

        add_stylesheet(req, 'common/css/browser.css')
        return 'browser.html', data

    # Internal methods

    def _render_repository_index(self, context, all_repositories, order, desc):
        # Color scale for the age column
        timerange = custom_colorizer = None
        if self.color_scale:
            custom_colorizer = self.get_custom_colorizer()

        rm = RepositoryManager(self.env)
        repositories = []
        for reponame, repoinfo in all_repositories.iteritems():
            if not reponame or as_bool(repoinfo.get('hidden')):
                continue
            try:
                repos = rm.get_repository(reponame)
            except TracError as err:
                entry = (reponame, repoinfo, None, None,
                         exception_to_unicode(err), None)
            else:
                if repos:
                    if not repos.is_viewable(context.perm):
                        continue
                    try:
                        youngest = repos.get_changeset(repos.youngest_rev)
                    except NoSuchChangeset:
                        youngest = None
                    if self.color_scale and youngest:
                        if not timerange:
                            timerange = TimeRange(youngest.date)
                        else:
                            timerange.insert(youngest.date)
                    raw_href = self._get_download_href(context.href, repos,
                                                       None, None)
                    entry = (reponame, repoinfo, repos, youngest, None,
                             raw_href)
                else:
                    entry = (reponame, repoinfo, None, None, u"\u2013", None)
            if entry[4] is not None:  # Check permission in case of error
                root = Resource('repository', reponame).child(self.realm, '/')
                if 'BROWSER_VIEW' not in context.perm(root):
                    continue
            repositories.append(entry)

        # Ordering of repositories
        if order == 'date':
            def repo_order(args):
                reponame, repoinfo, repos, youngest, err, href = args
                return (youngest.date if youngest else to_datetime(0),
                        embedded_numbers(reponame.lower()))
        elif order == 'author':
            def repo_order(args):
                reponame, repoinfo, repos, youngest, err, href = args
                return (youngest.author.lower() if youngest else '',
                        embedded_numbers(reponame.lower()))
        else:
            def repo_order(args):
                reponame, repoinfo, repos, youngest, err, href = args
                return embedded_numbers(reponame.lower())

        repositories = sorted(repositories, key=repo_order, reverse=desc)

        return {'repositories' : repositories,
                'timerange': timerange, 'colorize_age': custom_colorizer}

    def _render_dir(self, req, repos, node, rev, order, desc):
        req.perm(node.resource).require('BROWSER_VIEW')
        download_href = self._get_download_href

        # Entries metadata
        class entry(object):
            _copy = 'name rev created_rev kind isdir path content_length' \
                    .split()
            __slots__ = _copy + ['raw_href']

            def __init__(self, node):
                for f in entry._copy:
                    setattr(self, f, getattr(node, f))
                self.raw_href = download_href(req.href, repos, node, rev)

        entries = [entry(n) for n in node.get_entries()
                   if n.is_viewable(req.perm)]
        changes = get_changes(repos, [i.created_rev for i in entries],
                              self.log)

        if rev:
            newest = repos.get_changeset(rev).date
        else:
            newest = datetime_now(req.tz)

        # Color scale for the age column
        timerange = custom_colorizer = None
        if self.color_scale:
            timerange = TimeRange(newest)
            max_s = req.args.get('range_max_secs')
            min_s = req.args.get('range_min_secs')
            parent_range = [timerange.from_seconds(int(s))
                            for s in [max_s, min_s] if s]
            this_range = [c.date for c in changes.values() if c]
            for dt in this_range + parent_range:
                timerange.insert(dt)
            custom_colorizer = self.get_custom_colorizer()

        # Ordering of entries
        if order == 'date':
            def file_order(a):
                return (changes[a.created_rev].date,
                        embedded_numbers(a.name.lower()))
        elif order == 'size':
            def file_order(a):
                return (a.content_length,
                        embedded_numbers(a.name.lower()))
        elif order == 'author':
            def file_order(a):
                return (changes[a.created_rev].author.lower(),
                        embedded_numbers(a.name.lower()))
        else:
            def file_order(a):
                return embedded_numbers(a.name.lower())

        dir_order = 1 if desc else -1

        def browse_order(a):
            return dir_order if a.isdir else 0, file_order(a)
        entries = sorted(entries, key=browse_order, reverse=desc)

        # ''Zip Archive'' alternate link
        zip_href = self._get_download_href(req.href, repos, node, rev)
        if zip_href:
            add_link(req, 'alternate', zip_href, _('Zip Archive'),
                     'application/zip', 'zip')

        return {'entries': entries, 'changes': changes,
                'timerange': timerange, 'colorize_age': custom_colorizer,
                'range_max_secs': (timerange and
                                   timerange.to_seconds(timerange.newest)),
                'range_min_secs': (timerange and
                                   timerange.to_seconds(timerange.oldest)),
                }

    def _iter_nodes(self, node):
        stack = [node]
        while stack:
            node = stack.pop()
            yield node
            if node.isdir:
                stack.extend(sorted(node.get_entries(),
                                    key=lambda x: x.name,
                                    reverse=True))

    def _render_zip(self, req, context, repos, root_node, rev=None):
        if not self.is_path_downloadable(repos, root_node.path):
            raise TracError(_("Path not available for download"))
        req.perm(context.resource).require('FILE_VIEW')
        root_path = root_node.path.rstrip('/')
        if root_path:
            archive_name = root_node.name
        else:
            archive_name = repos.reponame or 'repository'
        filename = '%s-%s.zip' % (archive_name, root_node.rev)
        render_zip(req, filename, repos, root_node, self._iter_nodes)

    def _render_file(self, req, context, repos, node, rev=None):
        req.perm(node.resource).require('FILE_VIEW')

        mimeview = Mimeview(self.env)

        # MIME type detection
        with content_closing(node.get_processed_content()) as content:
            chunk = content.read(CHUNK_SIZE)
            mime_type = node.content_type
            if not mime_type or mime_type == 'application/octet-stream':
                mime_type = mimeview.get_mimetype(node.name, chunk) or \
                            mime_type or 'text/plain'

            # Eventually send the file directly
            format = req.args.get('format')
            if format in ('raw', 'txt'):
                req.send_response(200)
                req.send_header('Content-Type',
                                'text/plain' if format == 'txt' else mime_type)
                req.send_header('Last-Modified', http_date(node.last_modified))
                if rev is None:
                    req.send_header('Pragma', 'no-cache')
                    req.send_header('Cache-Control', 'no-cache')
                    req.send_header('Expires', 'Fri, 01 Jan 1999 00:00:00 GMT')
                if not self.render_unsafe_content:
                    # Force browser to download files instead of rendering
                    # them, since they might contain malicious code enabling
                    # XSS attacks
                    req.send_header('Content-Disposition', 'attachment')
                req.end_headers()
                # Note: don't pass an iterable instance to RequestDone, instead
                # call req.write() with each chunk here to avoid SEGVs (#11805)
                while chunk:
                    req.write(chunk)
                    chunk = content.read(CHUNK_SIZE)
                raise RequestDone

        # The changeset corresponding to the last change on `node`
        # is more interesting than the `rev` changeset.
        changeset = repos.get_changeset(node.created_rev)

        # add ''Plain Text'' alternate link if needed
        if not is_binary(chunk) and mime_type != 'text/plain':
            plain_href = req.href.browser(repos.reponame or None,
                                          node.path, rev=rev,
                                          format='txt')
            add_link(req, 'alternate', plain_href, _('Plain Text'),
                     'text/plain')

        # add ''Original Format'' alternate link (always)
        raw_href = req.href.export(rev or repos.youngest_rev,
                                   repos.reponame or None, node.path)
        add_link(req, 'alternate', raw_href, _('Original Format'),
                 mime_type)

        self.log.debug("Rendering preview of node %s@%s with "
                       "mime-type %s", node.name, rev, mime_type)

        add_stylesheet(req, 'common/css/code.css')

        annotations = ['lineno']
        annotate = req.args.get('annotate')
        if annotate:
            annotations.insert(0, annotate)
        with content_closing(node.get_processed_content()) as content:
            preview_data = mimeview.preview_data(context, content,
                                                 node.get_content_length(),
                                                 mime_type, node.created_path,
                                                 raw_href,
                                                 annotations=annotations,
                                                 force_source=bool(annotate))
        return {
            'changeset': changeset,
            'size': node.content_length,
            'preview': preview_data,
            'annotate': annotate,
            }

    def _get_download_href(self, href, repos, node, rev):
        """Return the URL for downloading a file, or a directory as a ZIP."""
        if node is not None and node.isfile:
            return href.export(rev or 'HEAD', repos.reponame or None,
                               node.path)
        path = '' if node is None else node.path.strip('/')
        if self.is_path_downloadable(repos, path):
            return href.browser(repos.reponame or None, path,
                                rev=rev or repos.youngest_rev, format='zip')

    # public methods

    def is_path_downloadable(self, repos, path):
        if repos.reponame:
            path = repos.reponame + '/' + path
        return any(fnmatchcase(path, dp.strip('/'))
                   for dp in self.downloadable_paths)

    def render_properties(self, mode, context, props):
        """Prepare rendering of a collection of properties."""
        return filter(None, [self.render_property(name, mode, context, props)
                             for name in sorted(props)])

    def render_property(self, name, mode, context, props):
        """Renders a node property to HTML."""
        if name in self.hidden_properties:
            return
        candidates = []
        for renderer in self.property_renderers:
            quality = renderer.match_property(name, mode)
            if quality > 0:
                candidates.append((quality, renderer))
        candidates.sort(reverse=True)
        for (quality, renderer) in candidates:
            try:
                rendered = renderer.render_property(name, mode, context, props)
                if not rendered:
                    return rendered
                if isinstance(rendered, RenderedProperty):
                    value = rendered.content
                else:
                    value = rendered
                    rendered = None
                prop = {'name': name, 'value': value, 'rendered': rendered}
                return prop
            except Exception as e:
                self.log.warning('Rendering failed for property %s with '
                                 'renderer %s: %s', name,
                                 renderer.__class__.__name__,
                                 exception_to_unicode(e, traceback=True))

    # IWikiSyntaxProvider methods

    def get_wiki_syntax(self):
        return []

    def get_link_resolvers(self):
        """TracBrowser link resolvers.

        `source:` and `browser:`
         * simple paths (/dir/file)
         * paths at a given revision (/dir/file@234)
         * paths with line number marks (/dir/file@234:10,20-30)
         * paths with line number anchor (/dir/file@234#L100)

        Marks and anchor can be combined.
        The revision must be present when specifying line numbers.
        In the few cases where it would be redundant (e.g. for tags), the
        revision number itself can be omitted: /tags/v10/file@100-110#L99
        """
        return [('repos', self._format_browser_link),
                ('export', self._format_export_link),
                ('source', self._format_browser_link),
                ('browser', self._format_browser_link)]

    def _format_export_link(self, formatter, ns, export, label):
        export, query, fragment = formatter.split_link(export)
        if ':' in export:
            rev, path = export.split(':', 1)
        elif '@' in export:
            path, rev = export.split('@', 1)
        else:
            rev, path = None, export
        node, raw_href, title = self._get_link_info(path, rev, formatter.href,
                                                    formatter.perm)
        if raw_href:
            return tag.a(label, class_='export', href=raw_href + fragment,
                         title=title)
        return tag.a(label, class_='missing export')

    def _format_browser_link(self, formatter, ns, path, label):
        path, query, fragment = formatter.split_link(path)
        rev = marks = None
        match = self.PATH_LINK_RE.match(path)
        if match:
            path, rev, marks = match.groups()
        href = formatter.href
        src_href = href.browser(path, rev=rev, marks=marks) + query + fragment
        node, raw_href, title = self._get_link_info(path, rev, formatter.href,
                                                    formatter.perm)
        if not node:
            return tag.a(label, class_='missing source')
        link = tag.a(label, class_='source', href=src_href)
        if raw_href:
            link = tag(link, tag.a(u'\u200b', href=raw_href + fragment,
                                   title=title,
                                   class_='trac-rawlink' if node.isfile
                                          else 'trac-ziplink'))
        return link

    PATH_LINK_RE = re.compile(r"([^@#:]*)"     # path
                              r"[@:]([^#:]+)?" # rev
                              r"(?::(\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*))?" # marks
                              )

    def _get_link_info(self, path, rev, href, perm):
        rm = RepositoryManager(self.env)
        node = raw_href = title = None
        try:
            reponame, repos, npath = rm.get_repository_by_path(path)
            node = get_allowed_node(repos, npath, rev, perm)
            if node is not None:
                raw_href = self._get_download_href(href, repos, node, rev)
                title = _("Download") if node.isfile \
                        else _("Download as Zip archive")
        except TracError:
            pass
        return node, raw_href, title

    # IHTMLPreviewAnnotator methods

    def get_annotation_type(self):
        return 'blame', _('Rev'), _('Revision in which the line changed')

    def get_annotation_data(self, context):
        """Cache the annotation data corresponding to each revision."""
        return BlameAnnotator(self.env, context)

    def annotate_row(self, context, row, lineno, line, blame_annotator):
        blame_annotator.annotate(row, lineno)

    # IWikiMacroProvider methods

    def get_macros(self):
        yield "RepositoryIndex"

    def get_macro_description(self, name):
        description = cleandoc_("""
        Display the list of available repositories.

        Can be given the following named arguments:

          ''format''::
            Select the rendering format:
            - ''compact'' produces a comma-separated list of repository prefix
              names (default)
            - ''list'' produces a description list of repository prefix names
            - ''table'' produces a table view, similar to the one visible in
              the ''Browse View'' page
          ''glob''::
            Do a glob-style filtering on the repository names (defaults to '*')
          ''order''::
            Order repositories by the given column (one of "name", "date" or
            "author")
          ''desc''::
            When set to 1, order by descending order
        """)
        return 'messages', description

    def expand_macro(self, formatter, name, content):
        args, kwargs = parse_args(content)
        format = kwargs.get('format', 'compact')
        glob = kwargs.get('glob', '*')
        order = kwargs.get('order')
        desc = as_bool(kwargs.get('desc', 0))

        rm = RepositoryManager(self.env)
        all_repos = dict(rdata for rdata in rm.get_all_repositories().items()
                         if fnmatchcase(rdata[0], glob))

        if format == 'table':
            repo = self._render_repository_index(formatter.context, all_repos,
                                                 order, desc)

            add_stylesheet(formatter.req, 'common/css/browser.css')
            wiki_format_messages = self.config['changeset'] \
                                       .getbool('wiki_format_messages')
            data = {'repo': repo, 'order': order, 'desc': 1 if desc else None,
                    'reponame': None, 'path': '/', 'stickyrev': None,
                    'wiki_format_messages': wiki_format_messages}
            return Chrome(self.env).render_fragment(formatter.context.req,
                                                    'repository_index.html',
                                                    data)

        def get_repository(reponame):
            try:
                return rm.get_repository(reponame)
            except TracError:
                return

        all_repos = [(reponame, get_repository(reponame))
                     for reponame in all_repos]
        all_repos = sorted(((reponame, repos) for reponame, repos in all_repos
                            if repos
                            and not as_bool(repos.params.get('hidden'))
                            and repos.is_viewable(formatter.perm)),
                           reverse=desc)

        def repolink(reponame, repos):
            label = reponame or _('(default)')
            return Markup(tag.a(label,
                          title=_('View repository %(repo)s', repo=label),
                          href=formatter.href.browser(repos.reponame or None)))

        if format == 'list':
            return tag.dl([
                tag(tag.dt(repolink(reponame, repos)),
                    tag.dd(repos.params.get('description')))
                for reponame, repos in all_repos])
        else: # compact
            return Markup(', ').join(repolink(reponame, repos)
                                     for reponame, repos in all_repos)
コード例 #25
0
class TicketSystem(Component):
    implements(IPermissionRequestor, IWikiSyntaxProvider, IResourceManager,
               ITicketManipulator)

    change_listeners = ExtensionPoint(ITicketChangeListener)
    milestone_change_listeners = ExtensionPoint(IMilestoneChangeListener)

    realm = 'ticket'

    ticket_custom_section = ConfigSection(
        'ticket-custom',
        """In this section, you can define additional fields for tickets. See
        TracTicketsCustomFields for more details.""")

    action_controllers = OrderedExtensionsOption(
        'ticket',
        'workflow',
        ITicketActionController,
        default='ConfigurableTicketWorkflow',
        include_missing=False,
        doc="""Ordered list of workflow controllers to use for ticket actions.
            """)

    restrict_owner = BoolOption(
        'ticket', 'restrict_owner', 'false',
        """Make the owner field of tickets use a drop-down menu.
        Be sure to understand the performance implications before activating
        this option. See
        [TracTickets#Assign-toasDrop-DownList Assign-to as Drop-Down List].

        Please note that e-mail addresses are '''not''' obfuscated in the
        resulting drop-down menu, so this option should not be used if
        e-mail addresses must remain protected.
        """)

    default_version = Option('ticket', 'default_version', '',
                             """Default version for newly created tickets.""")

    default_type = Option('ticket', 'default_type', 'defect',
                          """Default type for newly created tickets.""")

    default_priority = Option(
        'ticket', 'default_priority', 'major',
        """Default priority for newly created tickets.""")

    default_milestone = Option(
        'ticket', 'default_milestone', '',
        """Default milestone for newly created tickets.""")

    default_component = Option(
        'ticket', 'default_component', '',
        """Default component for newly created tickets.""")

    default_severity = Option(
        'ticket', 'default_severity', '',
        """Default severity for newly created tickets.""")

    default_summary = Option(
        'ticket', 'default_summary', '',
        """Default summary (title) for newly created tickets.""")

    default_description = Option(
        'ticket', 'default_description', '',
        """Default description for newly created tickets.""")

    default_keywords = Option(
        'ticket', 'default_keywords', '',
        """Default keywords for newly created tickets.""")

    default_owner = Option(
        'ticket', 'default_owner', '< default >',
        """Default owner for newly created tickets. The component owner
        is used when set to the value `< default >`.
        """)

    default_cc = Option('ticket', 'default_cc', '',
                        """Default cc: list for newly created tickets.""")

    default_resolution = Option(
        'ticket', 'default_resolution', 'fixed',
        """Default resolution for resolving (closing) tickets.""")

    allowed_empty_fields = ListOption(
        'ticket',
        'allowed_empty_fields',
        'milestone, version',
        doc="""Comma-separated list of `select` fields that can have
        an empty value. (//since 1.1.2//)""")

    max_comment_size = IntOption(
        'ticket', 'max_comment_size', 262144,
        """Maximum allowed comment size in characters.""")

    max_description_size = IntOption(
        'ticket', 'max_description_size', 262144,
        """Maximum allowed description size in characters.""")

    max_summary_size = IntOption(
        'ticket', 'max_summary_size', 262144,
        """Maximum allowed summary size in characters. (//since 1.0.2//)""")

    def __init__(self):
        self.log.debug('action controllers for ticket workflow: %r',
                       [c.__class__.__name__ for c in self.action_controllers])

    # Public API

    def get_available_actions(self, req, ticket):
        """Returns a sorted list of available actions"""
        # The list should not have duplicates.
        actions = {}
        for controller in self.action_controllers:
            weighted_actions = controller.get_ticket_actions(req, ticket) or []
            for weight, action in weighted_actions:
                if action in actions:
                    actions[action] = max(actions[action], weight)
                else:
                    actions[action] = weight
        all_weighted_actions = [(weight, action)
                                for action, weight in actions.items()]
        return [x[1] for x in sorted(all_weighted_actions, reverse=True)]

    def get_all_status(self):
        """Returns a sorted list of all the states all of the action
        controllers know about."""
        valid_states = set()
        for controller in self.action_controllers:
            valid_states.update(controller.get_all_status() or [])
        return sorted(valid_states)

    def get_ticket_field_labels(self):
        """Produce a (name,label) mapping from `get_ticket_fields`."""
        labels = {f['name']: f['label'] for f in self.get_ticket_fields()}
        labels['attachment'] = _("Attachment")
        return labels

    def get_ticket_fields(self):
        """Returns list of fields available for tickets.

        Each field is a dict with at least the 'name', 'label' (localized)
        and 'type' keys.
        It may in addition contain the 'custom' key, the 'optional' and the
        'options' keys. When present 'custom' and 'optional' are always `True`.
        """
        fields = copy.deepcopy(self.fields)
        label = 'label'  # workaround gettext extraction bug
        for f in fields:
            if not f.get('custom'):
                f[label] = gettext(f[label])
        return fields

    def reset_ticket_fields(self):
        """Invalidate ticket field cache."""
        del self.fields

    @cached
    def fields(self):
        """Return the list of fields available for tickets."""
        from trac.ticket import model

        fields = TicketFieldList()

        # Basic text fields
        fields.append({
            'name': 'summary',
            'type': 'text',
            'label': N_('Summary')
        })
        fields.append({
            'name': 'reporter',
            'type': 'text',
            'label': N_('Reporter')
        })

        # Owner field, by default text but can be changed dynamically
        # into a drop-down depending on configuration (restrict_owner=true)
        fields.append({'name': 'owner', 'type': 'text', 'label': N_('Owner')})

        # Description
        fields.append({
            'name': 'description',
            'type': 'textarea',
            'format': 'wiki',
            'label': N_('Description')
        })

        # Default select and radio fields
        selects = [('type', N_('Type'), model.Type),
                   ('status', N_('Status'), model.Status),
                   ('priority', N_('Priority'), model.Priority),
                   ('milestone', N_('Milestone'), model.Milestone),
                   ('component', N_('Component'), model.Component),
                   ('version', N_('Version'), model.Version),
                   ('severity', N_('Severity'), model.Severity),
                   ('resolution', N_('Resolution'), model.Resolution)]
        for name, label, cls in selects:
            options = [val.name for val in cls.select(self.env)]
            if not options:
                # Fields without possible values are treated as if they didn't
                # exist
                continue
            field = {
                'name': name,
                'type': 'select',
                'label': label,
                'value': getattr(self, 'default_' + name, ''),
                'options': options
            }
            if name in ('status', 'resolution'):
                field['type'] = 'radio'
                field['optional'] = True
            elif name in self.allowed_empty_fields:
                field['optional'] = True
            fields.append(field)

        # Advanced text fields
        fields.append({
            'name': 'keywords',
            'type': 'text',
            'format': 'list',
            'label': N_('Keywords')
        })
        fields.append({
            'name': 'cc',
            'type': 'text',
            'format': 'list',
            'label': N_('Cc')
        })

        # Date/time fields
        fields.append({
            'name': 'time',
            'type': 'time',
            'format': 'relative',
            'label': N_('Created')
        })
        fields.append({
            'name': 'changetime',
            'type': 'time',
            'format': 'relative',
            'label': N_('Modified')
        })

        for field in self.custom_fields:
            if field['name'] in [f['name'] for f in fields]:
                self.log.warning('Duplicate field name "%s" (ignoring)',
                                 field['name'])
                continue
            fields.append(field)

        return fields

    reserved_field_names = [
        'report', 'order', 'desc', 'group', 'groupdesc', 'col', 'row',
        'format', 'max', 'page', 'verbose', 'comment', 'or', 'id', 'time',
        'changetime', 'owner', 'reporter', 'cc', 'summary', 'description',
        'keywords'
    ]

    def get_custom_fields(self):
        return copy.deepcopy(self.custom_fields)

    @cached
    def custom_fields(self):
        """Return the list of custom ticket fields available for tickets."""
        fields = TicketFieldList()
        config = self.ticket_custom_section
        for name in [
                option for option, value in config.options()
                if '.' not in option
        ]:
            field = {
                'name':
                name,
                'custom':
                True,
                'type':
                config.get(name),
                'order':
                config.getint(name + '.order', 0),
                'label':
                config.get(name + '.label')
                or name.replace("_", " ").strip().capitalize(),
                'value':
                config.get(name + '.value', '')
            }
            if field['type'] == 'select' or field['type'] == 'radio':
                field['options'] = config.getlist(name + '.options', sep='|')
                if not field['options']:
                    continue
                if '' in field['options'] or \
                        field['name'] in self.allowed_empty_fields:
                    field['optional'] = True
                    if '' in field['options']:
                        field['options'].remove('')
            elif field['type'] == 'checkbox':
                field['value'] = '1' if as_bool(field['value']) else '0'
            elif field['type'] == 'text':
                field['format'] = config.get(name + '.format', 'plain')
                field['max_size'] = config.getint(name + '.max_size', 0)
            elif field['type'] == 'textarea':
                field['format'] = config.get(name + '.format', 'plain')
                field['max_size'] = config.getint(name + '.max_size', 0)
                field['height'] = config.getint(name + '.rows')
            elif field['type'] == 'time':
                field['format'] = config.get(name + '.format', 'datetime')

            if field['name'] in self.reserved_field_names:
                self.log.warning(
                    'Field name "%s" is a reserved name '
                    '(ignoring)', field['name'])
                continue
            if not re.match('^[a-zA-Z][a-zA-Z0-9_]+$', field['name']):
                self.log.warning(
                    'Invalid name for custom field: "%s" '
                    '(ignoring)', field['name'])
                continue

            fields.append(field)

        fields.sort(key=lambda f: (f['order'], f['name']))
        return fields

    def get_field_synonyms(self):
        """Return a mapping from field name synonyms to field names.
        The synonyms are supposed to be more intuitive for custom queries."""
        # i18n TODO - translated keys
        return {'created': 'time', 'modified': 'changetime'}

    def eventually_restrict_owner(self, field, ticket=None):
        """Restrict given owner field to be a list of users having
        the TICKET_MODIFY permission (for the given ticket)
        """
        if self.restrict_owner:
            field['type'] = 'select'
            field['options'] = self.get_allowed_owners(ticket)
            field['optional'] = True

    def get_allowed_owners(self, ticket=None):
        """Returns a list of permitted ticket owners (those possessing the
        TICKET_MODIFY permission). Returns `None` if the option `[ticket]`
        `restrict_owner` is `False`.

        If `ticket` is not `None`, fine-grained permission checks are used
        to determine the allowed owners for the specified resource.

        :since: 1.0.3
        """
        if self.restrict_owner:
            allowed_owners = []
            for user in PermissionSystem(self.env) \
                        .get_users_with_permission('TICKET_MODIFY'):
                if not ticket or \
                        'TICKET_MODIFY' in PermissionCache(self.env, user,
                                                           ticket.resource):
                    allowed_owners.append(user)
            allowed_owners.sort()
            return allowed_owners

    # ITicketManipulator methods

    def prepare_ticket(self, req, ticket, fields, actions):
        pass

    def validate_ticket(self, req, ticket):
        # Validate select fields for known values.
        for field in ticket.fields:
            if 'options' not in field:
                continue
            name = field['name']
            if name == 'status':
                continue
            if name in ticket and name in ticket._old:
                value = ticket[name]
                if value:
                    if value not in field['options']:
                        yield name, _('"%(value)s" is not a valid value',
                                      value=value)
                elif not field.get('optional', False):
                    yield name, _("field cannot be empty")

        # Validate description length.
        if len(ticket['description'] or '') > self.max_description_size:
            yield 'description', _(
                "Must be less than or equal to %(num)s "
                "characters",
                num=self.max_description_size)

        # Validate summary length.
        if not ticket['summary']:
            yield 'summary', _("Tickets must contain a summary.")
        elif len(ticket['summary'] or '') > self.max_summary_size:
            yield 'summary', _(
                "Must be less than or equal to %(num)s "
                "characters",
                num=self.max_summary_size)

        # Validate custom field length.
        for field in ticket.custom_fields:
            field_attrs = ticket.fields.by_name(field)
            max_size = field_attrs.get('max_size', 0)
            if 0 < max_size < len(ticket[field] or ''):
                label = field_attrs.get('label')
                yield label or field, _(
                    "Must be less than or equal to "
                    "%(num)s characters",
                    num=max_size)

        # Validate time field content.
        for field in ticket.time_fields:
            value = ticket[field]
            if field in ticket.custom_fields and \
                    field in ticket._old and \
                    not isinstance(value, datetime):
                field_attrs = ticket.fields.by_name(field)
                format = field_attrs.get('format')
                try:
                    ticket[field] = user_time(req, parse_date, value,
                                              hint=format) \
                                    if value else None
                except TracError as e:
                    # Degrade TracError to warning.
                    ticket[field] = value
                    label = field_attrs.get('label')
                    yield label or field, to_unicode(e)

    def validate_comment(self, req, comment):
        # Validate comment length
        if len(comment or '') > self.max_comment_size:
            yield _("Must be less than or equal to %(num)s characters",
                    num=self.max_comment_size)

    # IPermissionRequestor methods

    def get_permission_actions(self):
        return [
            'TICKET_APPEND', 'TICKET_CREATE', 'TICKET_CHGPROP', 'TICKET_VIEW',
            'TICKET_EDIT_CC', 'TICKET_EDIT_DESCRIPTION', 'TICKET_EDIT_COMMENT',
            ('TICKET_MODIFY', ['TICKET_APPEND', 'TICKET_CHGPROP']),
            ('TICKET_ADMIN', [
                'TICKET_CREATE', 'TICKET_MODIFY', 'TICKET_VIEW',
                'TICKET_EDIT_CC', 'TICKET_EDIT_DESCRIPTION',
                'TICKET_EDIT_COMMENT'
            ])
        ]

    # IWikiSyntaxProvider methods

    def get_link_resolvers(self):
        return [('bug', self._format_link), ('issue', self._format_link),
                ('ticket', self._format_link),
                ('comment', self._format_comment_link)]

    def get_wiki_syntax(self):
        yield (
            # matches #... but not &#... (HTML entity)
            r"!?(?<!&)#"
            # optional intertrac shorthand #T... + digits
            r"(?P<it_ticket>%s)%s" %
            (WikiParser.INTERTRAC_SCHEME, Ranges.RE_STR),
            lambda x, y, z: self._format_link(x, 'ticket', y[1:], y, z))

    def _format_link(self, formatter, ns, target, label, fullmatch=None):
        intertrac = formatter.shorthand_intertrac_helper(
            ns, target, label, fullmatch)
        if intertrac:
            return intertrac
        try:
            link, params, fragment = formatter.split_link(target)
            r = Ranges(link)
            if len(r) == 1:
                num = r.a
                ticket = formatter.resource(self.realm, num)
                from trac.ticket.model import Ticket
                if Ticket.id_is_valid(num) and \
                        'TICKET_VIEW' in formatter.perm(ticket):
                    # TODO: attempt to retrieve ticket view directly,
                    #       something like: t = Ticket.view(num)
                    for type, summary, status, resolution in \
                            self.env.db_query("""
                            SELECT type, summary, status, resolution
                            FROM ticket WHERE id=%s
                            """, (str(num),)):
                        description = self.format_summary(
                            summary, status, resolution, type)
                        title = '#%s: %s' % (num, description)
                        href = formatter.href.ticket(num) + params + fragment
                        return tag.a(label,
                                     title=title,
                                     href=href,
                                     class_='%s ticket' % status)
            else:
                ranges = str(r)
                if params:
                    params = '&' + params[1:]
                label_wrap = label.replace(',', u',\u200b')
                ranges_wrap = ranges.replace(',', u', ')
                return tag.a(label_wrap,
                             title=_("Tickets %(ranges)s", ranges=ranges_wrap),
                             href=formatter.href.query(id=ranges) + params)
        except ValueError:
            pass
        return tag.a(label, class_='missing ticket')

    def _format_comment_link(self, formatter, ns, target, label):
        resource = None
        if ':' in target:
            elts = target.split(':')
            if len(elts) == 3:
                cnum, realm, id = elts
                if cnum != 'description' and cnum and not cnum[0].isdigit():
                    realm, id, cnum = elts  # support old comment: style
                id = as_int(id, None)
                if realm in ('bug', 'issue'):
                    realm = 'ticket'
                resource = formatter.resource(realm, id)
        else:
            resource = formatter.resource
            cnum = target

        if resource and resource.id and resource.realm == self.realm and \
                cnum and (cnum.isdigit() or cnum == 'description'):
            href = title = class_ = None
            if self.resource_exists(resource):
                from trac.ticket.model import Ticket
                ticket = Ticket(self.env, resource.id)
                if cnum != 'description' and not ticket.get_change(cnum):
                    title = _("ticket comment does not exist")
                    class_ = 'missing ticket'
                elif 'TICKET_VIEW' in formatter.perm(resource):
                    href = formatter.href.ticket(resource.id) + \
                           "#comment:%s" % cnum
                    if resource.id != formatter.resource.id:
                        summary = self.format_summary(ticket['summary'],
                                                      ticket['status'],
                                                      ticket['resolution'],
                                                      ticket['type'])
                        if cnum == 'description':
                            title = _("Description for #%(id)s: %(summary)s",
                                      id=resource.id,
                                      summary=summary)
                        else:
                            title = _(
                                "Comment %(cnum)s for #%(id)s: "
                                "%(summary)s",
                                cnum=cnum,
                                id=resource.id,
                                summary=summary)
                        class_ = ticket['status'] + ' ticket'
                    else:
                        title = _("Description") if cnum == 'description' \
                                                 else _("Comment %(cnum)s",
                                                        cnum=cnum)
                        class_ = 'ticket'
                else:
                    title = _("no permission to view ticket")
                    class_ = 'forbidden ticket'
            else:
                title = _("ticket does not exist")
                class_ = 'missing ticket'
            return tag.a(label, class_=class_, href=href, title=title)
        return label

    # IResourceManager methods

    def get_resource_realms(self):
        yield self.realm

    def get_resource_description(self,
                                 resource,
                                 format=None,
                                 context=None,
                                 **kwargs):
        if format == 'compact':
            return '#%s' % resource.id
        elif format == 'summary':
            from trac.ticket.model import Ticket
            ticket = Ticket(self.env, resource.id)
            args = [
                ticket[f] for f in ('summary', 'status', 'resolution', 'type')
            ]
            return self.format_summary(*args)
        return _("Ticket #%(shortname)s", shortname=resource.id)

    def format_summary(self, summary, status=None, resolution=None, type=None):
        summary = shorten_line(summary)
        if type:
            summary = type + ': ' + summary
        if status:
            if status == 'closed' and resolution:
                status += ': ' + resolution
            return "%s (%s)" % (summary, status)
        else:
            return summary

    def resource_exists(self, resource):
        """
        >>> from trac.test import EnvironmentStub
        >>> from trac.resource import Resource, resource_exists
        >>> env = EnvironmentStub()

        >>> resource_exists(env, Resource('ticket', 123456))
        False

        >>> from trac.ticket.model import Ticket
        >>> t = Ticket(env)
        >>> int(t.insert())
        1
        >>> resource_exists(env, t.resource)
        True
        """
        try:
            id_ = int(resource.id)
        except (TypeError, ValueError):
            return False
        if self.env.db_query("SELECT id FROM ticket WHERE id=%s", (id_, )):
            if resource.version is None:
                return True
            revcount = self.env.db_query(
                """
                SELECT count(DISTINCT time) FROM ticket_change WHERE ticket=%s
                """, (id_, ))
            return revcount[0][0] >= resource.version
        else:
            return False
コード例 #26
0
ファイル: roadmap.py プロジェクト: vidhya-maddisetty/trac
class MilestoneModule(Component):
    """View and edit individual milestones."""

    implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
               IResourceManager, ISearchSource, ITimelineEventProvider,
               IWikiSyntaxProvider)

    realm = 'milestone'

    stats_provider = ExtensionOption(
        'milestone', 'stats_provider', ITicketGroupStatsProvider,
        'DefaultTicketGroupStatsProvider',
        """Name of the component implementing `ITicketGroupStatsProvider`,
        which is used to collect statistics on groups of tickets for display
        in the milestone views.""")

    default_retarget_to = Option(
        'milestone',
        'default_retarget_to',
        doc="""Default milestone to which tickets are retargeted when
            closing or deleting a milestone. (''since 1.1.2'')""")

    default_group_by = Option(
        'milestone', 'default_group_by', 'component',
        """Default field to use for grouping tickets in the grouped
        progress bar. (''since 1.2'')""")

    # INavigationContributor methods

    def get_active_navigation_item(self, req):
        return 'roadmap'

    def get_navigation_items(self, req):
        return []

    # IPermissionRequestor methods

    def get_permission_actions(self):
        actions = [
            'MILESTONE_CREATE', 'MILESTONE_DELETE', 'MILESTONE_MODIFY',
            'MILESTONE_VIEW'
        ]
        return actions + [('MILESTONE_ADMIN', actions)]

    # ITimelineEventProvider methods

    def get_timeline_filters(self, req):
        if 'MILESTONE_VIEW' in req.perm:
            yield ('milestone', _("Milestones completed"))

    def get_timeline_events(self, req, start, stop, filters):
        if 'milestone' in filters:
            milestone_realm = Resource(self.realm)
            for name, due, completed, description \
                    in MilestoneCache(self.env).milestones.itervalues():
                if completed and start <= completed <= stop:
                    # TODO: creation and (later) modifications should also be
                    #       reported
                    milestone = milestone_realm(id=name)
                    if 'MILESTONE_VIEW' in req.perm(milestone):
                        yield (
                            'milestone',
                            completed,
                            '',  # FIXME: author?
                            (milestone, description))

            # Attachments
            for event in AttachmentModule(self.env).get_timeline_events(
                    req, milestone_realm, start, stop):
                yield event

    def render_timeline_event(self, context, field, event):
        milestone, description = event[3]
        if field == 'url':
            return context.href.milestone(milestone.id)
        elif field == 'title':
            return tag_("Milestone %(name)s completed",
                        name=tag.em(milestone.id))
        elif field == 'description':
            child_resource = context.child(resource=milestone)
            return format_to(self.env, None, child_resource, description)

    # IRequestHandler methods

    def match_request(self, req):
        match = re.match(r'/milestone(?:/(.+))?$', req.path_info)
        if match:
            if match.group(1):
                req.args['id'] = match.group(1)
            return True

    def process_request(self, req):
        milestone_id = req.args.get('id')
        req.perm(self.realm, milestone_id).require('MILESTONE_VIEW')

        add_link(req, 'up', req.href.roadmap(), _("Roadmap"))

        action = req.args.get('action', 'view')
        try:
            milestone = Milestone(self.env, milestone_id)
        except ResourceNotFound:
            if 'MILESTONE_CREATE' not in req.perm(self.realm, milestone_id):
                raise
            milestone = Milestone(self.env)
            milestone.name = milestone_id
            action = 'edit'  # rather than 'new', so it works for POST/save

        if req.method == 'POST':
            if 'cancel' in req.args:
                if milestone.exists:
                    req.redirect(req.href.milestone(milestone.name))
                else:
                    req.redirect(req.href.roadmap())
            elif action == 'edit':
                return self._do_save(req, milestone)
            elif action == 'delete':
                self._do_delete(req, milestone)
            else:
                raise HTTPBadRequest(_("Invalid request arguments."))
        elif action in ('new', 'edit'):
            return self._render_editor(req, milestone)
        elif action == 'delete':
            return self._render_confirm(req, milestone)

        if not milestone.name:
            req.redirect(req.href.roadmap())

        return self._render_view(req, milestone)

    # Public methods

    def get_default_due(self, req):
        """Returns a `datetime` object representing the default due date in
        the user's timezone. The default due time is 18:00 in the user's
        time zone.
        """
        now = datetime_now(req.tz)
        default_due = datetime(now.year, now.month, now.day, 18)
        if now.hour > 18:
            default_due += timedelta(days=1)
        return to_datetime(default_due, req.tz)

    def save_milestone(self, req, milestone):
        # Instead of raising one single error, check all the constraints
        # and let the user fix them by going back to edit mode and showing
        # the warnings
        warnings = []

        def warn(msg):
            add_warning(req, msg)
            warnings.append(msg)

        milestone.description = req.args.get('description', '')

        if 'due' in req.args:
            duedate = req.args.get('duedate')
            milestone.due = user_time(req, parse_date, duedate,
                                      hint='datetime') \
                            if duedate else None
        else:
            milestone.due = None

        # -- check completed date
        if 'completed' in req.args:
            completed = req.args.get('completeddate', '')
            completed = user_time(req, parse_date, completed,
                                  hint='datetime') if completed else None
            if completed and completed > datetime_now(utc):
                warn(_("Completion date may not be in the future"))
        else:
            completed = None
        milestone.completed = completed

        # -- check the name
        # If the name has changed, check that the milestone doesn't already
        # exist
        # FIXME: the whole .exists business needs to be clarified
        #        (#4130) and should behave like a WikiPage does in
        #        this respect.
        new_name = req.args.get('name')
        try:
            new_milestone = Milestone(self.env, new_name)
        except ResourceNotFound:
            milestone.name = new_name
        else:
            if new_milestone.name != milestone.name:
                if new_milestone.name:
                    warn(
                        _(
                            'Milestone "%(name)s" already exists, please '
                            'choose another name.',
                            name=new_milestone.name))
                else:
                    warn(_("You must provide a name for the milestone."))

        if warnings:
            return False

        # -- actually save changes
        if milestone.exists:
            milestone.update(author=req.authname)
            if completed and 'retarget' in req.args:
                comment = req.args.get('comment', '')
                retarget_to = req.args.get('target') or None
                retargeted_tickets = \
                    milestone.move_tickets(retarget_to, req.authname,
                                           comment, exclude_closed=True)
                add_notice(
                    req,
                    _(
                        'The open tickets associated with '
                        'milestone "%(name)s" have been retargeted '
                        'to milestone "%(retarget)s".',
                        name=milestone.name,
                        retarget=retarget_to))
                new_values = {'milestone': retarget_to}
                comment = comment or \
                          _("Open tickets retargeted after milestone closed")
                event = BatchTicketChangeEvent(retargeted_tickets, None,
                                               req.authname, comment,
                                               new_values, None)
                try:
                    NotificationSystem(self.env).notify(event)
                except Exception as e:
                    self.log.error(
                        "Failure sending notification on ticket "
                        "batch change: %s", exception_to_unicode(e))
                    add_warning(
                        req,
                        tag_(
                            "The changes have been saved, but "
                            "an error occurred while sending "
                            "notifications: %(message)s",
                            message=to_unicode(e)))
            add_notice(req, _("Your changes have been saved."))
        else:
            milestone.insert()
            add_notice(
                req,
                _('The milestone "%(name)s" has been added.',
                  name=milestone.name))

        return True

    # Internal methods

    _default_retarget_to = default_retarget_to

    @property
    def default_retarget_to(self):
        if self._default_retarget_to and \
           not any(self._default_retarget_to == m.name
                   for m in Milestone.select(self.env)):
            self.log.warn(
                'Milestone "%s" does not exist. Update the '
                '"default_retarget_to" option in the [milestone] '
                'section of trac.ini', self._default_retarget_to)
        return self._default_retarget_to

    def _do_delete(self, req, milestone):
        req.perm(milestone.resource).require('MILESTONE_DELETE')

        retarget_to = req.args.get('target') or None
        # Don't translate ticket comment (comment:40:ticket:5658)
        retargeted_tickets = \
            milestone.move_tickets(retarget_to, req.authname,
                "Ticket retargeted after milestone deleted")
        milestone.delete()
        add_notice(
            req,
            _('The milestone "%(name)s" has been deleted.',
              name=milestone.name))
        if retargeted_tickets:
            add_notice(
                req,
                _(
                    'The tickets associated with milestone '
                    '"%(name)s" have been retargeted to milestone '
                    '"%(retarget)s".',
                    name=milestone.name,
                    retarget=retarget_to))
            new_values = {'milestone': retarget_to}
            comment = _("Tickets retargeted after milestone deleted")
            event = BatchTicketChangeEvent(retargeted_tickets, None,
                                           req.authname, comment, new_values,
                                           None)
            try:
                NotificationSystem(self.env).notify(event)
            except Exception as e:
                self.log.error(
                    "Failure sending notification on ticket batch "
                    "change: %s", exception_to_unicode(e))
                add_warning(
                    req,
                    tag_(
                        "The changes have been saved, but an "
                        "error occurred while sending "
                        "notifications: %(message)s",
                        message=to_unicode(e)))

        req.redirect(req.href.roadmap())

    def _do_save(self, req, milestone):
        if milestone.exists:
            req.perm(milestone.resource).require('MILESTONE_MODIFY')
        else:
            req.perm(milestone.resource).require('MILESTONE_CREATE')

        if self.save_milestone(req, milestone):
            req.redirect(req.href.milestone(milestone.name))

        return self._render_editor(req, milestone)

    def _render_confirm(self, req, milestone):
        req.perm(milestone.resource).require('MILESTONE_DELETE')

        milestones = [
            m for m in Milestone.select(self.env) if m.name != milestone.name
            and 'MILESTONE_VIEW' in req.perm(m.resource)
        ]
        attachments = Attachment.select(self.env, self.realm, milestone.name)
        data = {
            'milestone':
            milestone,
            'milestone_groups':
            group_milestones(milestones, 'TICKET_ADMIN' in req.perm),
            'num_tickets':
            get_num_tickets_for_milestone(self.env, milestone),
            'retarget_to':
            self.default_retarget_to,
            'attachments':
            list(attachments)
        }
        add_stylesheet(req, 'common/css/roadmap.css')
        return 'milestone_delete.html', data, None

    def _render_editor(self, req, milestone):
        data = {
            'milestone': milestone,
            'datetime_hint': get_datetime_format_hint(req.lc_time),
            'default_due': self.get_default_due(req),
            'milestone_groups': [],
        }

        if milestone.exists:
            req.perm(milestone.resource).require('MILESTONE_MODIFY')
            milestones = [
                m for m in Milestone.select(self.env)
                if m.name != milestone.name
                and 'MILESTONE_VIEW' in req.perm(m.resource)
            ]
            data['milestone_groups'] = \
                group_milestones(milestones, 'TICKET_ADMIN' in req.perm)
            data['num_open_tickets'] = \
                get_num_tickets_for_milestone(self.env, milestone,
                                              exclude_closed=True)
            data['retarget_to'] = self.default_retarget_to
        else:
            req.perm(milestone.resource).require('MILESTONE_CREATE')
            if milestone.name:
                add_notice(
                    req,
                    _(
                        "Milestone %(name)s does not exist. You "
                        "can create it here.",
                        name=milestone.name))

        chrome = Chrome(self.env)
        chrome.add_jquery_ui(req)
        chrome.add_wiki_toolbars(req)
        add_stylesheet(req, 'common/css/roadmap.css')
        return 'milestone_edit.html', data, None

    def _render_view(self, req, milestone):
        milestone_groups = []
        available_groups = []
        default_group_by_available = False
        ticket_fields = TicketSystem(self.env).get_ticket_fields()

        # collect fields that can be used for grouping
        for field in ticket_fields:
            if field['type'] == 'select' and field['name'] != 'milestone' \
                    or field['name'] in ('owner', 'reporter'):
                available_groups.append({
                    'name': field['name'],
                    'label': field['label']
                })
                if field['name'] == self.default_group_by:
                    default_group_by_available = True

        # determine the field currently used for grouping
        by = None
        if default_group_by_available:
            by = self.default_group_by
        elif available_groups:
            by = available_groups[0]['name']
        by = req.args.get('by', by)

        tickets = get_tickets_for_milestone(self.env,
                                            milestone=milestone.name,
                                            field=by)
        tickets = apply_ticket_permissions(self.env, req, tickets)
        stat = get_ticket_stats(self.stats_provider, tickets)

        context = web_context(req, milestone.resource)
        data = {
            'context': context,
            'milestone': milestone,
            'attachments': AttachmentModule(self.env).attachment_data(context),
            'available_groups': available_groups,
            'grouped_by': by,
            'groups': milestone_groups
        }
        data.update(milestone_stats_data(self.env, req, stat, milestone.name))

        if by:

            def per_group_stats_data(gstat, group_name):
                return milestone_stats_data(self.env, req, gstat,
                                            milestone.name, by, group_name)

            milestone_groups.extend(
                grouped_stats_data(self.env, self.stats_provider, tickets, by,
                                   per_group_stats_data))

        add_stylesheet(req, 'common/css/roadmap.css')
        add_script(req, 'common/js/folding.js')

        def add_milestone_link(rel, milestone):
            href = req.href.milestone(milestone.name, by=req.args.get('by'))
            add_link(req, rel, href,
                     _('Milestone "%(name)s"', name=milestone.name))

        milestones = [
            m for m in Milestone.select(self.env)
            if 'MILESTONE_VIEW' in req.perm(m.resource)
        ]
        idx = [i for i, m in enumerate(milestones) if m.name == milestone.name]
        if idx:
            idx = idx[0]
            if idx > 0:
                add_milestone_link('first', milestones[0])
                add_milestone_link('prev', milestones[idx - 1])
            if idx < len(milestones) - 1:
                add_milestone_link('next', milestones[idx + 1])
                add_milestone_link('last', milestones[-1])
        prevnext_nav(req, _("Previous Milestone"), _("Next Milestone"),
                     _("Back to Roadmap"))

        return 'milestone_view.html', data, None

    # IWikiSyntaxProvider methods

    def get_wiki_syntax(self):
        return []

    def get_link_resolvers(self):
        yield ('milestone', self._format_link)

    def _format_link(self, formatter, ns, name, label):
        name, query, fragment = formatter.split_link(name)
        return self._render_link(formatter.context, name, label,
                                 query + fragment)

    def _render_link(self, context, name, label, extra=''):
        if not (name or extra):
            return tag()
        try:
            milestone = Milestone(self.env, name)
        except ResourceNotFound:
            milestone = None
        # Note: the above should really not be needed, `Milestone.exists`
        # should simply be false if the milestone doesn't exist in the db
        # (related to #4130)
        href = context.href.milestone(name)
        if milestone and milestone.exists:
            if 'MILESTONE_VIEW' in context.perm(milestone.resource):
                title = None
                if hasattr(context, 'req'):
                    if milestone.is_completed:
                        title = _("Completed %(duration)s ago (%(date)s)",
                                  duration=pretty_timedelta(
                                      milestone.completed),
                                  date=user_time(context.req, format_datetime,
                                                 milestone.completed))
                    elif milestone.is_late:
                        title = _("%(duration)s late (%(date)s)",
                                  duration=pretty_timedelta(milestone.due),
                                  date=user_time(context.req, format_datetime,
                                                 milestone.due))
                    elif milestone.due:
                        title = _("Due in %(duration)s (%(date)s)",
                                  duration=pretty_timedelta(milestone.due),
                                  date=user_time(context.req, format_datetime,
                                                 milestone.due))
                    else:
                        title = _("No date set")
                closed = 'closed ' if milestone.is_completed else ''
                return tag.a(label,
                             class_='%smilestone' % closed,
                             href=href + extra,
                             title=title)
        elif 'MILESTONE_CREATE' in context.perm(self.realm, name):
            return tag.a(label,
                         class_='missing milestone',
                         href=href + extra,
                         rel='nofollow')
        return tag.a(label, class_='missing milestone')

    # IResourceManager methods

    def get_resource_realms(self):
        yield self.realm

    def get_resource_description(self,
                                 resource,
                                 format=None,
                                 context=None,
                                 **kwargs):
        desc = resource.id
        if format != 'compact':
            desc = _("Milestone %(name)s", name=resource.id)
        if context:
            return self._render_link(context, resource.id, desc)
        else:
            return desc

    def resource_exists(self, resource):
        """
        >>> from trac.test import EnvironmentStub
        >>> env = EnvironmentStub()

        >>> m1 = Milestone(env)
        >>> m1.name = 'M1'
        >>> m1.insert()

        >>> MilestoneModule(env).resource_exists(Resource('milestone', 'M1'))
        True
        >>> MilestoneModule(env).resource_exists(Resource('milestone', 'M2'))
        False
        """
        return resource.id in MilestoneCache(self.env).milestones

    # ISearchSource methods

    def get_search_filters(self, req):
        if 'MILESTONE_VIEW' in req.perm:
            yield ('milestone', _("Milestones"))

    def get_search_results(self, req, terms, filters):
        if 'milestone' not in filters:
            return
        term_regexps = search_to_regexps(terms)
        milestone_realm = Resource(self.realm)
        for name, due, completed, description \
                in MilestoneCache(self.env).milestones.itervalues():
            if all(
                    r.search(description) or r.search(name)
                    for r in term_regexps):
                milestone = milestone_realm(id=name)
                if 'MILESTONE_VIEW' in req.perm(milestone):
                    dt = (completed
                          if completed else due if due else datetime_now(utc))
                    yield (get_resource_url(self.env, milestone, req.href),
                           get_resource_name(self.env, milestone), dt, '',
                           shorten_result(description, terms))

        # Attachments
        for result in AttachmentModule(self.env).get_search_results(
                req, milestone_realm, terms):
            yield result
コード例 #27
0
def get_estimation_field():
    return Option('estimation-tools',
                  'estimation_field',
                  'estimatedhours',
                  doc="""Defines what custom field should be used to calculate
        estimation charts. Defaults to 'estimatedhours'""")
コード例 #28
0
ファイル: env.py プロジェクト: t2y/trac
class Environment(Component, ComponentManager):
    """Trac environment manager.

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

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

    """

    implements(ISystemInfoProvider)

    required = True

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

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

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

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

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

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

        See also: TracPlugins
        """)

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

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

        (''since 0.11'')""")

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    log_type = Option('logging', 'log_type', 'none',
        """Logging facility to use.

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

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

    log_level = Option('logging', 'log_level', 'DEBUG',
        """Level of verbosity in log.

        Should be one of (`CRITICAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`).""")

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

        If nothing is set, the following will be used:

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

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

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

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

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

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

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

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

        self.path = path
        self.log = None
        self.config = None
        # System info should be provided through ISystemInfoProvider rather
        # than appending to systeminfo, which may be a private in a future
        # release.
        self.systeminfo = []

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

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

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

    def get_configinfo(self):
        """Returns a list of dictionaries containing the `name` and `options`
        of each configuration section. The value of `options` is a list of
        dictionaries containing the `name`, `value` and `modified` state of
        each configuration option. The `modified` value is True if the value
        differs from its default.

        :since: version 1.1.2
        """
        defaults = self.config.defaults(self.compmgr)
        sections = []
        for section in self.config.sections(self.compmgr):
            options = []
            default_options = defaults.get(section, {})
            for name, value in self.config.options(section, self.compmgr):
                default = default_options.get(name) or ''
                options.append({
                    'name': name, 'value': value,
                    'modified': unicode(value) != unicode(default)
                })
            options.sort(key=lambda o: o['name'])
            sections.append({'name': section, 'options': options})
        sections.sort(key=lambda s: s['name'])
        return sections

    # ISystemInfoProvider methods

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

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

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

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

    @lazy
    def _component_rules(self):
        _rules = {}
        for name, value in self.components_section.options():
            if name.endswith('.*'):
                name = name[:-2]
            _rules[name.lower()] = value.lower() in ('enabled', 'on')
        return _rules

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

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

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

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

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

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

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

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

        :deprecated: Use :meth:`db_transaction` or :meth:`db_query` instead

        `db_transaction` for obtaining the `db` database connection
        which can be used for performing any query
        (SELECT/INSERT/UPDATE/DELETE)::

           with env.db_transaction as db:
               ...

        Note that within the block, you don't need to (and shouldn't)
        call ``commit()`` yourself, the context manager will take care
        of it (if it's the outermost such context manager on the
        stack).


        `db_query` for obtaining a `db` database connection which can
        be used for performing SELECT queries only::

           with env.db_query as db:
               ...
        """
        return DatabaseManager(self).get_connection()

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

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

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

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

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

        See `trac.db.api.get_read_db` for detailed documentation."""
        return DatabaseManager(self).get_connection(readonly=True)

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

        Example::

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

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

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

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

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

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

        """
        return QueryContextManager(self)

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

        Example::

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

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

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

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

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

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

            env.db_transaction("UPDATE ...")

        """
        return TransactionContextManager(self)

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

    def get_repository(self, reponame=None, authname=None):
        """Return the version control repository with the given name,
        or the default repository if `None`.

        The standard way of retrieving repositories is to use the
        methods of `RepositoryManager`. This method is retained here
        for backward compatibility.

        :param reponame: the name of the repository
        :param authname: the user name for authorization (not used
                         anymore, left here for compatibility with
                         0.11)
        """
        return RepositoryManager(self).get_repository(reponame)

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

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

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

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

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

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

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

        :since: 0.11
        """
        rows = self.db_query("""
                SELECT value FROM system WHERE name='%sdatabase_version'
                """ % ('initial_' if initial else ''))
        return int(rows[0][0]) if rows else False

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

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

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

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

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

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

        This function generates one tuple for every user, of the form
        (username, name, email) ordered alpha-numerically by username.
        """
        for username, name, email in self.db_query("""
                SELECT DISTINCT s.sid, n.value, e.value
                FROM session AS s
                 LEFT JOIN session_attribute AS n ON (n.sid=s.sid
                  and n.authenticated=1 AND n.name = 'name')
                 LEFT JOIN session_attribute AS e ON (e.sid=s.sid
                  AND e.authenticated=1 AND e.name = 'email')
                WHERE s.authenticated=1 ORDER BY s.sid
                """):
            yield username, name, email

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

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

    def needs_upgrade(self):
        """Return whether the environment needs to be upgraded."""
        for participant in self.setup_participants:
            args = ()
            with self.db_query as db:
                if arity(participant.environment_needs_upgrade) == 1:
                    args = (db,)
                if participant.environment_needs_upgrade(*args):
                    self.log.warn("Component %s requires environment upgrade",
                                  participant)
                    return True
        return False

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

        :param backup: whether or not to backup before upgrading
        :param backup_dest: name of the backup file
        :return: whether the upgrade was performed
        """
        upgraders = []
        for participant in self.setup_participants:
            args = ()
            with self.db_query as db:
                if arity(participant.environment_needs_upgrade) == 1:
                    args = (db,)
                if participant.environment_needs_upgrade(*args):
                    upgraders.append(participant)
        if not upgraders:
            return

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

        for participant in upgraders:
            self.log.info("%s.%s upgrading...", participant.__module__,
                          participant.__class__.__name__)
            args = ()
            with self.db_transaction as db:
                if arity(participant.upgrade_environment) == 1:
                    args = (db,)
                participant.upgrade_environment(*args)
            # Database schema may have changed, so close all connections
            DatabaseManager(self).shutdown()
        return True

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

    @lazy
    def abs_href(self):
        """The application URL"""
        if not self.base_url:
            self.log.warn("base_url option not set in configuration, "
                          "generated links may be incorrect")
            _abs_href = Href('')
        else:
            _abs_href = Href(self.base_url)
        return _abs_href
コード例 #29
0
class AuthzSourcePolicy(Component):
    """Permission policy for `source:` and `changeset:` resources using a
    Subversion authz file.
    
    `FILE_VIEW` and `BROWSER_VIEW` permissions are granted as specified in the
    authz file.
    
    `CHANGESET_VIEW` permission is granted for changesets where `FILE_VIEW` is
    granted on at least one modified file, as well as for empty changesets.
    """

    implements(IPermissionPolicy)

    authz_file = PathOption(
        'trac', 'authz_file', '', """The path to the Subversion
        [http://svnbook.red-bean.com/en/1.5/svn.serverconfig.pathbasedauthz.html authorization (authz) file].
        To enable authz permission checking, the `AuthzSourcePolicy` permission
        policy must be added to `[trac] permission_policies`.
        """)

    authz_module_name = Option(
        'trac', 'authz_module_name', '',
        """The module prefix used in the `authz_file` for the default
        repository. If left empty, the global section is used.
        """)

    _mtime = 0
    _authz = {}
    _users = set()

    _handled_perms = frozenset([(None, 'BROWSER_VIEW'),
                                (None, 'CHANGESET_VIEW'), (None, 'FILE_VIEW'),
                                (None, 'LOG_VIEW'), ('source', 'BROWSER_VIEW'),
                                ('source', 'FILE_VIEW'),
                                ('source', 'LOG_VIEW'),
                                ('changeset', 'CHANGESET_VIEW')])

    # IPermissionPolicy methods

    def check_permission(self, action, username, resource, perm):
        realm = resource.realm if resource else None
        if (realm, action) in self._handled_perms:
            authz, users = self._get_authz_info()
            if authz is None:
                return False

            if username == 'anonymous':
                usernames = ('$anonymous', '*')
            else:
                usernames = (username, '$authenticated', '*')
            if resource is None:
                return True if users & set(usernames) else None

            rm = RepositoryManager(self.env)
            try:
                repos = rm.get_repository(resource.parent.id)
            except TracError:
                return True  # Allow error to be displayed in the repo index
            if repos is None:
                return True
            modules = [resource.parent.id or self.authz_module_name]
            if modules[0]:
                modules.append('')

            def check_path(path):
                path = '/' + join(repos.scope, path)
                if path != '/':
                    path += '/'

                # Walk from resource up parent directories
                for spath in parent_iter(path):
                    for module in modules:
                        section = authz.get(module, {}).get(spath)
                        if section:
                            for user in usernames:
                                result = section.get(user)
                                if result is not None:
                                    return result

                # Allow access to parent directories of allowed resources
                if any(
                        section.get(user) is True
                        for module in modules for spath, section in authz.get(
                            module, {}).iteritems() if spath.startswith(path)
                        for user in usernames):
                    return True

            if realm == 'source':
                return check_path(resource.id)

            elif realm == 'changeset':
                changes = list(repos.get_changeset(resource.id).get_changes())
                if not changes or any(
                        check_path(change[0]) for change in changes):
                    return True

    def _get_authz_info(self):
        try:
            mtime = os.path.getmtime(self.authz_file)
        except OSError, e:
            if self._authz is not None:
                self.log.error('Error accessing authz file: %s',
                               exception_to_unicode(e))
            self._mtime = mtime = 0
            self._authz = None
            self._users = set()
        if mtime > self._mtime:
            self._mtime = mtime
            rm = RepositoryManager(self.env)
            modules = set(repos.reponame
                          for repos in rm.get_real_repositories())
            if '' in modules and self.authz_module_name:
                modules.add(self.authz_module_name)
            modules.add('')
            self.log.info('Parsing authz file: %s' % self.authz_file)
            try:
                self._authz = parse(read_file(self.authz_file), modules)
                self._users = set(user for paths in self._authz.itervalues()
                                  for path in paths.itervalues()
                                  for user, result in path.iteritems()
                                  if result)
            except Exception, e:
                self._authz = None
                self._users = set()
                self.log.error('Error parsing authz file: %s',
                               exception_to_unicode(e))
コード例 #30
0
ファイル: api.py プロジェクト: wataash/trac
class DatabaseManager(Component):
    """Component used to manage the `IDatabaseConnector` implementations."""

    implements(IEnvironmentSetupParticipant, ISystemInfoProvider)

    connectors = ExtensionPoint(IDatabaseConnector)

    connection_uri = Option(
        'trac', 'database', 'sqlite:db/trac.db', """Database connection
        [wiki:TracEnvironment#DatabaseConnectionStrings string] for this
        project""")

    backup_dir = Option('trac', 'backup_dir', 'db',
                        """Database backup location""")

    timeout = IntOption(
        'trac', 'timeout', '20',
        """Timeout value for database connection, in seconds.
        Use '0' to specify ''no timeout''.""")

    debug_sql = BoolOption(
        'trac', 'debug_sql', False,
        """Show the SQL queries in the Trac log, at DEBUG level.
        """)

    def __init__(self):
        self._cnx_pool = None
        self._transaction_local = ThreadLocal(wdb=None, rdb=None)

    def init_db(self):
        connector, args = self.get_connector()
        args['schema'] = db_default.schema
        connector.init_db(**args)
        version = db_default.db_version
        self.set_database_version(version, 'initial_database_version')
        self.set_database_version(version)

    def insert_default_data(self):
        self.insert_into_tables(db_default.get_data)

    def destroy_db(self):
        connector, args = self.get_connector()
        # Connections to on-disk db must be closed before deleting it.
        self.shutdown()
        connector.destroy_db(**args)

    def db_exists(self):
        connector, args = self.get_connector()
        return connector.db_exists(**args)

    def create_tables(self, schema):
        """Create the specified tables.

        :param schema: an iterable of table objects.

        :since: version 1.0.2
        """
        connector = self.get_connector()[0]
        with self.env.db_transaction as db:
            for table in schema:
                for sql in connector.to_sql(table):
                    db(sql)

    def drop_columns(self, table, columns):
        """Drops the specified columns from table.

        :since: version 1.2
        """
        table_name = table.name if isinstance(table, Table) else table
        with self.env.db_transaction as db:
            if not db.has_table(table_name):
                raise self.env.db_exc.OperationalError('Table %s not found' %
                                                       db.quote(table_name))
            for col in columns:
                db.drop_column(table_name, col)

    def drop_tables(self, schema):
        """Drop the specified tables.

        :param schema: an iterable of `Table` objects or table names.

        :since: version 1.0.2
        """
        with self.env.db_transaction as db:
            for table in schema:
                table_name = table.name if isinstance(table, Table) else table
                db.drop_table(table_name)

    def insert_into_tables(self, data_or_callable):
        """Insert data into existing tables.

        :param data_or_callable: Nested tuples of table names, column names
                                 and row data::

                                   (table1,
                                    (column1, column2),
                                    ((row1col1, row1col2),
                                     (row2col1, row2col2)),
                                    table2, ...)

                                 or a callable that takes a single parameter
                                 `db` and returns the aforementioned nested
                                 tuple.
        :since: version 1.1.3
        """
        with self.env.db_transaction as db:
            data = data_or_callable(db) if callable(data_or_callable) \
                                        else data_or_callable
            for table, cols, vals in data:
                db.executemany(
                    "INSERT INTO %s (%s) VALUES (%s)" %
                    (db.quote(table), ','.join(cols), ','.join(
                        ['%s'] * len(cols))), vals)

    def reset_tables(self):
        """Deletes all data from the tables and resets autoincrement indexes.

        :return: list of names of the tables that were reset.

        :since: version 1.1.3
        """
        with self.env.db_transaction as db:
            return db.reset_tables()

    def upgrade_tables(self, new_schema):
        """Upgrade table schema to `new_schema`, preserving data in
        columns that exist in the current schema and `new_schema`.

        :param new_schema: tuple or list of `Table` objects

        :since: version 1.2
        """
        with self.env.db_transaction as db:
            cursor = db.cursor()
            for new_table in new_schema:
                temp_table_name = new_table.name + '_old'
                has_table = self.has_table(new_table)
                if has_table:
                    old_column_names = set(self.get_column_names(new_table))
                    new_column_names = {col.name for col in new_table.columns}
                    column_names = old_column_names & new_column_names
                    if column_names:
                        cols_to_copy = ','.join(
                            db.quote(name) for name in column_names)
                        cursor.execute("""
                            CREATE TEMPORARY TABLE %s AS SELECT * FROM %s
                            """ % (db.quote(temp_table_name),
                                   db.quote(new_table.name)))
                    self.drop_tables((new_table, ))
                self.create_tables((new_table, ))
                if has_table and column_names:
                    cursor.execute("""
                        INSERT INTO %s (%s) SELECT %s FROM %s
                        """ % (db.quote(new_table.name), cols_to_copy,
                               cols_to_copy, db.quote(temp_table_name)))
                    for col in new_table.columns:
                        if col.auto_increment:
                            db.update_sequence(cursor, new_table.name,
                                               col.name)
                    self.drop_tables((temp_table_name, ))

    def get_connection(self, readonly=False):
        """Get a database connection from the pool.

        If `readonly` is `True`, the returned connection will purposely
        lack the `rollback` and `commit` methods.
        """
        if not self._cnx_pool:
            connector, args = self.get_connector()
            self._cnx_pool = ConnectionPool(5, connector, **args)
        db = self._cnx_pool.get_cnx(self.timeout or None)
        if readonly:
            db = ConnectionWrapper(db, readonly=True)
        return db

    def get_database_version(self, name='database_version'):
        """Returns the database version from the SYSTEM table as an int,
        or `False` if the entry is not found.

        :param name: The name of the entry that contains the database version
                     in the SYSTEM table. Defaults to `database_version`,
                     which contains the database version for Trac.
        """
        with self.env.db_query as db:
            for value, in db(
                    """
                    SELECT value FROM {0} WHERE name=%s
                    """.format(db.quote('system')), (name, )):
                return int(value)
            else:
                return False

    def get_exceptions(self):
        return self.get_connector()[0].get_exceptions()

    def get_sequence_names(self):
        """Returns a list of the sequence names.

        :since: 1.3.2
        """
        with self.env.db_query as db:
            return db.get_sequence_names()

    def get_table_names(self):
        """Returns a list of the table names.

        :since: 1.1.6
        """
        with self.env.db_query as db:
            return db.get_table_names()

    def get_column_names(self, table):
        """Returns a list of the column names for `table`.

        :param table: a `Table` object or table name.

        :since: 1.2
        """
        table_name = table.name if isinstance(table, Table) else table
        with self.env.db_query as db:
            if not db.has_table(table_name):
                raise self.env.db_exc.OperationalError('Table %s not found' %
                                                       db.quote(table_name))
            return db.get_column_names(table_name)

    def has_table(self, table):
        """Returns whether the table exists."""
        table_name = table.name if isinstance(table, Table) else table
        with self.env.db_query as db:
            return db.has_table(table_name)

    def set_database_version(self, version, name='database_version'):
        """Sets the database version in the SYSTEM table.

        :param version: an integer database version.
        :param name: The name of the entry that contains the database version
                     in the SYSTEM table. Defaults to `database_version`,
                     which contains the database version for Trac.
        """
        current_database_version = self.get_database_version(name)
        if current_database_version is False:
            with self.env.db_transaction as db:
                db(
                    """
                    INSERT INTO {0} (name, value) VALUES (%s, %s)
                    """.format(db.quote('system')), (name, version))
        elif version != self.get_database_version(name):
            with self.env.db_transaction as db:
                db(
                    """
                    UPDATE {0} SET value=%s WHERE name=%s
                    """.format(db.quote('system')), (version, name))
            self.log.info("Upgraded %s from %d to %d", name,
                          current_database_version, version)

    def needs_upgrade(self, version, name='database_version'):
        """Checks the database version to determine if an upgrade is needed.

        :param version: the expected integer database version.
        :param name: the name of the entry in the SYSTEM table that contains
                     the database version. Defaults to `database_version`,
                     which contains the database version for Trac.

        :return: `True` if the stored version is less than the expected
                  version, `False` if it is equal to the expected version.
        :raises TracError: if the stored version is greater than the expected
                           version.
        """
        dbver = self.get_database_version(name)
        if dbver == version:
            return False
        elif dbver > version:
            raise TracError(_("Need to downgrade %(name)s.", name=name))
        self.log.info("Need to upgrade %s from %d to %d", name, dbver, version)
        return True

    def upgrade(self, version, name='database_version', pkg='trac.upgrades'):
        """Invokes `do_upgrade(env, version, cursor)` in module
        `"%s/db%i.py" % (pkg, version)`, for each required version upgrade.

        :param version: the expected integer database version.
        :param name: the name of the entry in the SYSTEM table that contains
                     the database version. Defaults to `database_version`,
                     which contains the database version for Trac.
        :param pkg: the package containing the upgrade modules.

        :raises TracError: if the package or module doesn't exist.
        """
        dbver = self.get_database_version(name)
        for i in xrange(dbver + 1, version + 1):
            module = '%s.db%i' % (pkg, i)
            try:
                upgrader = importlib.import_module(module)
            except ImportError:
                raise TracError(
                    _("No upgrade module %(module)s.py", module=module))
            with self.env.db_transaction as db:
                cursor = db.cursor()
                upgrader.do_upgrade(self.env, i, cursor)
                self.set_database_version(i, name)

    def shutdown(self, tid=None):
        if self._cnx_pool:
            self._cnx_pool.shutdown(tid)
            if not tid:
                self._cnx_pool = None

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

        :param dest: base filename to write to.

        Returns the file actually written.
        """
        connector, args = self.get_connector()
        if not dest:
            backup_dir = self.backup_dir
            if not os.path.isabs(backup_dir):
                backup_dir = os.path.join(self.env.path, backup_dir)
            db_str = self.config.get('trac', 'database')
            db_name, db_path = db_str.split(":", 1)
            dest_name = '%s.%i.%d.bak' % (db_name, self.env.database_version,
                                          int(time.time()))
            dest = os.path.join(backup_dir, dest_name)
        else:
            backup_dir = os.path.dirname(dest)
        if not os.path.exists(backup_dir):
            os.makedirs(backup_dir)
        return connector.backup(dest)

    def get_connector(self):
        scheme, args = parse_connection_uri(self.connection_uri)
        candidates = [
            (priority, connector) for connector in self.connectors
            for scheme_, priority in connector.get_supported_schemes()
            if scheme_ == scheme
        ]
        if not candidates:
            raise TracError(
                _('Unsupported database type "%(scheme)s"', scheme=scheme))
        priority, connector = max(candidates)
        if priority < 0:
            raise TracError(connector.error)

        if scheme == 'sqlite':
            if args['path'] == ':memory:':
                # Special case for SQLite in-memory database, always get
                # the /same/ connection over
                pass
            elif not os.path.isabs(args['path']):
                # Special case for SQLite to support a path relative to the
                # environment directory
                args['path'] = os.path.join(self.env.path,
                                            args['path'].lstrip('/'))

        if self.debug_sql:
            args['log'] = self.log
        return connector, args

    # IEnvironmentSetupParticipant methods

    def environment_created(self):
        pass

    def environment_needs_upgrade(self):
        return self.needs_upgrade(db_default.db_version)

    def upgrade_environment(self):
        self.upgrade(db_default.db_version)

    # ISystemInfoProvider methods

    def get_system_info(self):
        connector = self.get_connector()[0]
        for info in connector.get_system_info():
            yield info