Ticket #2423: macros.py

File macros.py, 5.5 kB (added by lei@mms-dresden.de, 5 months ago)

Headlines hierarchy patch

Line 
1 # TracIncludeMacro macros
2 import urllib2
3 import re
4 from StringIO import StringIO
5
6 from trac.core import *
7 from trac.wiki.macros import WikiMacroBase
8 from trac.wiki.formatter import system_message
9 from trac.wiki.model import WikiPage
10 from trac.mimeview.api import Mimeview, get_mimetype, Context
11 from trac.perm import IPermissionRequestor
12 from trac.util.html import html
13 from genshi.core import escape
14 from genshi.input import HTMLParser, ParseError
15 from genshi.filters.html import HTMLSanitizer
16
17 __all__ = ['IncludeMacro']
18
19 class IncludeMacro(WikiMacroBase):
20     """A macro to include other resources in wiki pages.
21     More documentation to follow.
22     """
23    
24     implements(IPermissionRequestor)
25    
26     # Default output formats for sources that need them
27     default_formats = {
28         'wiki': 'text/x-trac-wiki',
29     }
30    
31     # Increase of the hierarchy level
32     hierarchy_offset = None
33    
34     # IWikiMacroProvider methods
35     def render_macro(self, req, name, content):
36         args = [x.strip() for x in content.split(',')]
37         if len(args) == 1:
38             args.append(1)      # hierarchy_offset: if not given, it defaults to 1
39             args.append(None)   
40         elif len(args) == 2:
41             args.append(None)
42         elif len(args) != 3:
43             return system_message('Invalid arguments "%s"'%content)
44            
45         # Pull out the arguments
46         source, self.hierarchy_offset, dest_format = args
47        
48         try:
49             source_format, source_obj = source.split(':', 1)
50         except ValueError: # If no : is present, assume its a wiki page
51             source_format, source_obj = 'wiki', source
52            
53         # Apply a default format if needed
54         if dest_format is None:
55             try:
56                 dest_format = self.default_formats[source_format]
57             except KeyError:
58                 pass
59        
60         if source_format in ('http', 'https', 'ftp'):
61             # Since I can't really do recursion checking, and because this
62             # could be a source of abuse allow selectively blocking it.
63             # RFE: Allow blacklist/whitelist patterns for URLS. <NPK>
64             # RFE: Track page edits and prevent unauthorized users from ever entering a URL include. <NPK>
65             if not req.perm.has_permission('INCLUDE_URL'):
66                 self.log.info('IncludeMacro: Blocking attempt by %s to include URL %s on page %s', req.authname, source, req.path_info)
67                 return ''
68             try:
69                 urlf = urllib2.urlopen(source)
70                 out = urlf.read() 
71             except urllib2.URLError, e:
72                 return system_message('Error while retrieving file', str(e))
73             except TracError, e:
74                 return system_message('Error while previewing', str(e))
75             ctxt = Context.from_request(req)
76         elif source_format == 'wiki':
77             # XXX: Check for recursion in page includes. <NPK>
78             if not req.perm.has_permission('WIKI_VIEW'):
79                 return ''
80             page = WikiPage(self.env, source_obj)
81             if not page.exists:
82                 return system_message('Wiki page %s does not exist'%source_obj)
83             out = page.text                       
84            
85             # modify headlines       
86             out = re.sub('=+', self.add_equal_sign, out)
87            
88             # modify hierarchy level parameters in any existing IncludeMacros
89             out = re.sub('\[\[Include\([\w/]+(,[\w/-]+){0,2}\)\]\]', self.increase_incl_macro_param, out)
90            
91             ctxt = Context.from_request(req, 'wiki', source_obj)
92         elif source_format == 'source':
93             if not req.perm.has_permission('FILE_VIEW'):
94                 return ''
95             repo = self.env.get_repository(req.authname)
96             node = repo.get_node(source_obj)
97             out = node.get_content().read()
98             if dest_format is None:
99                 dest_format = node.content_type or get_mimetype(source_obj, out)
100             ctxt = Context.from_request(req, 'source', source_obj)
101         # RFE: Add ticket: and comment: sources. <NPK>
102         # RFE: Add attachment: source. <NPK>
103         else:
104             return system_message('Unsupported include source %s'%source)
105            
106         # If we have a preview format, use it
107         if dest_format:
108             out = Mimeview(self.env).render(ctxt, dest_format, out)
109        
110         # Escape if needed
111         if not self.config.getbool('wiki', 'render_unsafe_content', False):
112             try:
113                 out = HTMLParser(StringIO(out)).parse() | HTMLSanitizer()
114             except ParseError:
115                 out = escape(out)
116        
117         return out
118    
119     # add '=' to increase hierarchy level
120     def add_equal_sign(self, matchObj):
121         return matchObj.group(0) + "="*int(self.hierarchy_offset)
122    
123     # modify the hierarchy level parameter
124     def increase_incl_macro_param(self, matchObj):
125         # Lucas 2008-05-14
126        
127         split = re.split('[\(\),]', matchObj.group(0))
128         string = split[0] + "(" + split[1]
129    
130         if re.match('[1-7]', split[2]):
131             string += ',' + str((int(split[2]) +1))
132             i = 3
133         else:
134             string += ',2'
135             i = 2
136    
137         while split[i] != ']]':
138             string += ',' + split[i]
139             i += 1
140         else:
141             string += ')' + split[i]
142        
143         return string
144    
145            
146     # IPermissionRequestor methods
147     def get_permission_actions(self):
148         yield 'INCLUDE_URL'
149            
150        
151