source: tagsplugin/tags/0.4.1/tractags/macros.py

Last change on this file was 2268, checked in by Alec Thomas, 16 years ago

TagsPlugin:

Fix for XSS vulnerability. Closes #1581.

File size: 10.8 KB
Line 
1from trac.core import *
2from trac.wiki.api import IWikiMacroProvider
3from trac.wiki import model
4from trac.wiki import wiki_to_html, wiki_to_oneliner
5from StringIO import StringIO
6from tractags.api import TagEngine, ITagSpaceUser, sorted, set
7import inspect
8import re
9import string
10
11class TagMacros(Component):
12    """ Versions of the old Wiki-only macros using the new tag API. """
13
14    implements(IWikiMacroProvider)
15
16    def _page_titles(self, pages):
17        """ Extract page titles, if possible. """
18        titles = {}
19        tagspace = TagEngine(self.env).tagspace.wiki
20        for pagename in pages:
21            href, link, title = tagspace.name_details(pagename)
22            titles[pagename] = title
23        return titles
24
25    def _tag_details(self, links, tags):
26        """ Extract dictionary of tag:(href, title) for all tags. """
27        for tag in tags:
28            if tag not in links:
29                links[tag] = TagEngine(self.env).get_tag_link(tag)
30        return links
31
32    def _current_page(self, req):
33        return req.hdf.getValue('wiki.page_name', '')
34
35    # IWikiMacroProvider methods
36    def get_macros(self):
37        yield 'TagCloud'
38        yield 'ListTagged'
39        yield 'TagIt'
40        yield 'ListTags'
41
42    def get_macro_description(self, name):
43        import pydoc
44        return pydoc.getdoc(getattr(self, 'render_' + name.lower()))
45
46    def render_macro(self, req, name, content):
47        from trac.web.chrome import add_stylesheet
48        from parseargs import parseargs
49        add_stylesheet(req, 'tags/css/tractags.css')
50        # Translate macro args into python args
51        args = []
52        kwargs = {}
53        try:
54            # Set default args from config
55            _, config_args = parseargs(self.env.config.get('tags', '%s.args' % name.lower(), ''))
56            kwargs.update(config_args)
57            if content is not None:
58                args, macro_args = parseargs(content)
59                kwargs.update(macro_args)
60        except Exception, e:
61            raise TracError("Invalid arguments '%s' (%s %s)" % (content, e.__class__.__name__, e))
62
63        return getattr(self, 'render_' + name.lower(), content)(req, *args, **kwargs)
64
65    # Macro implementations
66    def render_tagcloud(self, req, smallest=10, biggest=20, showcount=True, tagspace=None, mincount=1, tagspaces=[]):
67        """ This macro displays a [http://en.wikipedia.org/wiki/Tag_cloud tag cloud] (weighted list)
68            of all tags.
69
70            ||'''Argument'''||'''Description'''||
71            ||`tagspace=<tagspace>`||Specify the tagspace the macro should operate on.||
72            ||`tagspaces=(<tagspace>,...)`||Specify a set of tagspaces the macro should operate on.||
73            ||`smallest=<n>`||The lower bound of the font size for the tag cloud.||
74            ||`biggest=<n>`||The upper bound of the font size for the tag cloud.||
75            ||`showcount=true|false`||Show the count of objects for each tag?||
76            ||`mincount=<n>`||Hide tags with a count less than `<n>`.||
77            """
78
79        smallest = int(smallest)
80        biggest = int(biggest)
81        mincount = int(mincount)
82
83        engine = TagEngine(self.env)
84        # Get wiki tagspace
85        if tagspace:
86            tagspaces = [tagspace]
87        else:
88            tagspaces = tagspaces or engine.tagspaces
89        cloud = {}
90
91        for tag, names in engine.get_tags(tagspaces=tagspaces, detailed=True).iteritems():
92            count = len(names)
93            if count >= mincount:
94                cloud[tag] = len(names)
95
96        tags = cloud.keys()
97
98        # No tags?
99        if not tags: return ''
100
101        # by_count maps tag counts to an index in the set of counts
102        by_count = list(set(cloud.values()))
103        by_count.sort()
104        by_count = dict([(c, float(i)) for i, c in enumerate(by_count)])
105
106        taginfo = self._tag_details({}, tags)
107        tags.sort()
108        rlen = float(biggest - smallest)
109        tlen = float(len(by_count))
110        scale = 1.0
111        if tlen:
112            scale = rlen / tlen
113        out = StringIO()
114        out.write('<ul class="tagcloud">\n')
115        last = tags[-1]
116        for tag in tags:
117            if tag == last:
118                cls = ' class="last"'
119            else:
120                cls = ''
121            if showcount != 'false':
122                count = ' <span class="tagcount">(%i)</span>' % cloud[tag]
123            else:
124                count = ''
125            out.write('<li%s><a rel="tag" title="%s" style="font-size: %ipx" href="%s">%s</a>%s</li>\n' % (
126                       cls,
127                       taginfo[tag][1] + ' (%i)' % cloud[tag],
128                       smallest + int(by_count[cloud[tag]] * scale),
129                       taginfo[tag][0],
130                       tag,
131                       count))
132        out.write('</ul>\n')
133        return out.getvalue()
134
135    def render_listtagged(self, req, *tags, **kwargs):
136        """ List tagged objects. Optionally accepts a list of tags to match
137            against.  The special tag '''. (dot)''' inserts the current Wiki page name.
138
139            `[[ListTagged(<tag>, ...)]]`
140
141            ||'''Argument'''||'''Description'''||
142            ||`tagspace=<tagspace>`||Specify the tagspace the macro should operate on.||
143            ||`tagspaces=(<tagspace>,...)`||Specify a set of tagspaces the macro should operate on.||
144            ||`operation=intersection|union`||The set operation to perform on the discovered objects.||
145            ||`showheadings=true|false`||List objects under the tagspace they occur in.||
146            ||`expression=<expr>`||Match object tags against the given expression.||
147
148            The supported expression operators are: unary - (not); binary +, -
149            and | (and, and not, or). All values in the expression are treated
150            as tags. Any tag not in the same form as a Python variable must be
151            quoted.
152           
153            eg. Match all objects tagged with ticket and workflow, and not
154            tagged with wiki or closed.
155           
156                (ticket+workflow)-(wiki|closed)
157
158            If an expression is provided operation is ignored.
159        """
160
161        if 'tagspace' in kwargs:
162            tagspaces = [kwargs.get('tagspace', None)]
163        else:
164            tagspaces = kwargs.get('tagspaces', '') or \
165                        list(TagEngine(self.env).tagspaces)
166        expression = kwargs.get('expression', None)
167        showheadings = kwargs.get('showheadings', 'false')
168        operation = kwargs.get('operation', 'intersection')
169        if operation not in ('union', 'intersection'):
170            raise TracError("Invalid tag set operation '%s'" % operation)
171
172        engine = TagEngine(self.env)
173        page_name = req.hdf.get('wiki.page_name')
174        if page_name:
175            tags = [tag == '.' and page_name or tag for tag in tags]
176
177        tags = set(tags)
178        taginfo = {}
179        out = StringIO()
180        out.write('<ul class="listtagged">\n')
181        # If expression was passed as an argument, do a full walk, using the
182        # expression as the predicate. Silently assumes that failed expressions
183        # are normal tags.
184        if expression:
185            from tractags.expr import Expression
186            try:
187                expr = Expression(expression)
188            except Exception, e:
189                self.env.log.error("Invalid expression '%s'" % expression, exc_info=True)
190                tags.update([x.strip() for x in re.split('[+,]', expression) if x.strip()])
191                expression = None
192            else:
193                self.env.log.debug(expr.ast)
194                tagged_names = {}
195                tags.update(expr.get_tags())
196                for tagspace, name, name_tags in engine.walk_tagged_names(tags=tags,
197                        tagspaces=tagspaces, predicate=lambda ts, n, t: expr(t)):
198                    tagged_names.setdefault(tagspace, {})[name] = name_tags
199                tagged_names = [(tagspace, names) for tagspace, names in tagged_names.iteritems()]
200
201        if not expression:
202            tagged_names = [(tagspace, names) for tagspace, names in
203                            engine.get_tagged_names(tags=tags, tagspaces=tagspaces,
204                                operation=operation, detailed=True).iteritems()
205                            if names]
206
207        for tagspace, tagspace_names in sorted(tagged_names):
208            if showheadings == 'true':
209                out.write('<lh>%s tags</lh>' % tagspace)
210            for name, tags in sorted(tagspace_names.iteritems()):
211                if tagspace == 'wiki' and unicode(name).startswith('tags/'): continue
212                tags = sorted(tags)
213                taginfo = self._tag_details(taginfo, tags)
214                href, link, title = engine.name_details(tagspace, name)
215                htitle = wiki_to_oneliner(title, self.env)
216                name_tags = ['<a href="%s" title="%s">%s</a>'
217                              % (taginfo[tag][0], taginfo[tag][1], tag)
218                              for tag in tags]
219                if not name_tags:
220                    name_tags = ''
221                else:
222                    name_tags = ' (' + ', '.join(sorted(name_tags)) + ')'
223                out.write('<li>%s %s%s</li>\n' %
224                          (link, htitle, name_tags))
225        out.write('</ul>')
226
227        return out.getvalue()
228
229    def render_tagit(self, req, *tags):
230        """ '''''Deprecated. Does nothing.''''' """
231        return ''
232
233    def render_listtags(self, req, *tags, **kwargs):
234        """ List all tags.
235
236            ||'''Argument'''||'''Description'''||
237            ||`tagspace=<tagspace>`||Specify the tagspace the macro should operate on.||
238            ||`tagspaces=(<tagspace>,...)`||Specify a set of tagspaces the macro should operate on.||
239            ||`shownames=true|false`||Whether to show the objects that tags appear on ''(long)''.||
240            """
241
242        if tags:
243            # Backwards compatibility
244            return self.render_listtagged(req, *tags, **kwargs)
245
246        page = self._current_page(req)
247        engine = TagEngine(self.env)
248
249        showpages = kwargs.get('showpages', None) or kwargs.get('shownames', 'false')
250
251        if 'tagspace' in kwargs:
252            tagspaces = [kwargs['tagspace']]
253        else:
254            tagspaces = kwargs.get('tagspaces', []) or \
255                        list(TagEngine(self.env).tagspaces)
256
257        out = StringIO()
258        out.write('<ul class="listtags">\n')
259        tag_details = {}
260        for tag, names in sorted(engine.get_tags(tagspaces=tagspaces, detailed=True).iteritems()):
261            href, title = engine.get_tag_link(tag)
262            htitle = wiki_to_oneliner(title, self.env)
263            out.write('<li><a href="%s" title="%s">%s</a> %s <span class="tagcount">(%i)</span>' % (href, title, tag, htitle, len(names)))
264            if showpages == 'true':
265                out.write('\n')
266                out.write(self.render_listtagged(req, tag, tagspaces=tagspaces))
267                out.write('</li>\n')
268
269        out.write('</ul>\n')
270
271        return out.getvalue()
Note: See TracBrowser for help on using the repository browser.