1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 import re
20
21 from genshi.builder import tag
22
23 from trac.cache import cached
24 from trac.config import BoolOption, ListOption
25 from trac.core import *
26 from trac.resource import IResourceManager
27 from trac.util.html import is_safe_origin
28 from trac.util.text import unquote_label
29 from trac.util.translation import _
30 from trac.wiki.parser import WikiParser
34 """Components that want to get notified about the creation,
35 deletion and modification of wiki pages should implement that
36 interface.
37 """
38
40 """Called whenever a new Wiki page is added."""
41
42 - def wiki_page_changed(page, version, t, comment, author, ipnr):
43 """Called when a page has been modified.
44
45 :since 1.0.3: `ipnr` is optional and deprecated, and will
46 be removed in 1.3.1
47 """
48
50 """Called when a page has been deleted."""
51
53 """Called when a version of a page has been deleted."""
54
55 - def wiki_page_renamed(page, old_name):
56 """Called when a page has been renamed."""
57
59 """Called when a page comment has been modified."""
60
61
62 -class IWikiPageManipulator(Interface):
63 """Components that need to do specific pre- and post- processing of
64 wiki page changes have to implement this interface.
65
66 Unlike change listeners, a manipulator can reject changes being
67 committed to the database.
68 """
69
70 - def prepare_wiki_page(req, page, fields):
71 """Validate a wiki page before rendering it.
72
73 :param page: is the `WikiPage` being viewed.
74
75 :param fields: is a dictionary which contains the wiki `text`
76 of the page, initially identical to `page.text` but it can
77 eventually be transformed in place before being used as
78 input to the formatter.
79 """
80
81 - def validate_wiki_page(req, page):
82 """Validate a wiki page after it's been populated from user input.
83
84 :param page: is the `WikiPage` being edited.
85
86 :return: a list of `(field, message)` tuples, one for each
87 problem detected. `field` can be `None` to indicate an
88 overall problem with the page. Therefore, a return value of
89 `[]` means everything is OK.
90 """
91
94 """Augment the Wiki markup with new Wiki macros.
95
96 .. versionchanged :: 0.12
97 new Wiki processors can also be added that way.
98 """
99
101 """Return an iterable that provides the names of the provided macros.
102 """
103
105 """Return a tuple of a domain name to translate and plain text
106 description of the macro or only the description with the specified
107 name.
108
109 .. versionchanged :: 1.0
110 `get_macro_description` can return a domain to translate the
111 description.
112 """
113
115 """Return `True` if the content generated is an inline XHTML element.
116
117 .. versionadded :: 1.0
118 """
119
121 """Called by the formatter when rendering the parsed wiki text.
122
123 .. versionadded:: 0.11
124
125 .. versionchanged:: 0.12
126 added the `args` parameter
127
128 :param formatter: the wiki `Formatter` currently processing
129 the wiki markup
130
131 :param name: is the name by which the macro has been called;
132 remember that via `get_macros`, multiple names could be
133 associated to this macros. Note that the macro names are
134 case sensitive.
135
136 :param content: is the content of the macro call. When called
137 using macro syntax (`[[Macro(content)]]`), this is the
138 string contained between parentheses, usually containing
139 macro arguments. When called using wiki processor syntax
140 (`{{{!#Macro ...}}}`), it is the content of the processor
141 block, that is, the text starting on the line following the
142 macro name.
143
144 :param args: will be a dictionary containing the named
145 parameters passed when using the Wiki processor syntax.
146
147 The named parameters can be specified when calling the macro
148 using the wiki processor syntax::
149
150 {{{#!Macro arg1=value1 arg2="value 2"`
151 ... some content ...
152 }}}
153
154 In this example, `args` will be
155 `{'arg1': 'value1', 'arg2': 'value 2'}`
156 and `content` will be `"... some content ..."`.
157
158 If no named parameters are given like in::
159
160 {{{#!Macro
161 ...
162 }}}
163
164 then `args` will be `{}`. That makes it possible to
165 differentiate the above situation from a call
166 made using the macro syntax::
167
168 [[Macro(arg1=value1, arg2="value 2", ... some content...)]]
169
170 in which case `args` will always be `None`. Here `content`
171 will be the
172 `"arg1=value1, arg2="value 2", ... some content..."` string.
173 If like in this example, `content` is expected to contain
174 some arguments and named parameters, one can use the
175 `parse_args` function to conveniently extract them.
176 """
177
180 """Enrich the Wiki syntax with new markup."""
181
183 """Return an iterable that provides additional wiki syntax.
184
185 Additional wiki syntax correspond to a pair of `(regexp, cb)`,
186 the `regexp` for the additional syntax and the callback `cb`
187 which will be called if there's a match. That function is of
188 the form `cb(formatter, ns, match)`.
189 """
190
192 """Return an iterable over `(namespace, formatter)` tuples.
193
194 Each formatter should be a function of the form::
195
196 def format(formatter, ns, target, label, fullmatch=None):
197 pass
198
199 and should return some HTML fragment. The `label` is already
200 HTML escaped, whereas the `target` is not. The `fullmatch`
201 argument is optional, and is bound to the regexp match object
202 for the link.
203 """
204
206 """Utility for parsing macro "content" and splitting them into arguments.
207
208 The content is split along commas, unless they are escaped with a
209 backquote (see example below).
210
211 :param args: a string containing macros arguments
212 :param strict: if `True`, only Python-like identifiers will be
213 recognized as keyword arguments
214
215 Example usage::
216
217 >>> parse_args('')
218 ([], {})
219 >>> parse_args('Some text')
220 (['Some text'], {})
221 >>> parse_args('Some text, mode= 3, some other arg\, with a comma.')
222 (['Some text', ' some other arg, with a comma.'], {'mode': ' 3'})
223 >>> parse_args('milestone=milestone1,status!=closed', strict=False)
224 ([], {'status!': 'closed', 'milestone': 'milestone1'})
225
226 """
227 largs, kwargs = [], {}
228 if args:
229 for arg in re.split(r'(?<!\\),', args):
230 arg = arg.replace(r'\,', ',')
231 if strict:
232 m = re.match(r'\s*[a-zA-Z_]\w+=', arg)
233 else:
234 m = re.match(r'\s*[^=]+=', arg)
235 if m:
236 kw = arg[:m.end()-1].strip()
237 if strict:
238 kw = unicode(kw).encode('utf-8')
239 kwargs[kw] = arg[m.end():]
240 else:
241 largs.append(arg)
242 return largs, kwargs
243
244
245 -def validate_page_name(pagename):
246 """Utility for validating wiki page name.
247
248 :param pagename: wiki page name to validate
249 """
250 return pagename and \
251 all(part not in ('', '.', '..') for part in pagename.split('/'))
252
255 """Wiki system manager."""
256
257 implements(IWikiSyntaxProvider, IResourceManager)
258
259 change_listeners = ExtensionPoint(IWikiChangeListener)
260 macro_providers = ExtensionPoint(IWikiMacroProvider)
261 syntax_providers = ExtensionPoint(IWikiSyntaxProvider)
262
263 realm = 'wiki'
264
265 ignore_missing_pages = BoolOption('wiki', 'ignore_missing_pages', 'false',
266 """Enable/disable highlighting CamelCase links to missing pages.
267 """)
268
269 split_page_names = BoolOption('wiki', 'split_page_names', 'false',
270 """Enable/disable splitting the WikiPageNames with space characters.
271 """)
272
273 render_unsafe_content = BoolOption('wiki', 'render_unsafe_content', 'false',
274 """Enable/disable the use of unsafe HTML tags such as `<script>` or
275 `<embed>` with the HTML [wiki:WikiProcessors WikiProcessor].
276
277 For public sites where anonymous users can edit the wiki it is
278 recommended to leave this option disabled.
279 """)
280
281 safe_schemes = ListOption('wiki', 'safe_schemes',
282 'cvs, file, ftp, git, irc, http, https, news, sftp, smb, ssh, svn, '
283 'svn+ssh',
284 doc="""List of URI schemes considered "safe", that will be rendered as
285 external links even if `[wiki] render_unsafe_content` is `false`.
286 """)
287
288 safe_origins = ListOption('wiki', 'safe_origins',
289 'data:',
290 doc="""List of URIs considered "safe cross-origin", that will be
291 rendered as `img` element without `crossorigin="anonymous"` attribute
292 or used in `url()` of inline style attribute even if
293 `[wiki] render_unsafe_content` is `false` (''since 1.0.15'').
294
295 To make any origins safe, specify "*" in the list.""")
296
297 @cached
299 """Return the names of all existing wiki pages."""
300 return set(name for name, in
301 self.env.db_query("SELECT DISTINCT name FROM wiki"))
302
303
304
305 - def get_pages(self, prefix=None):
306 """Iterate over the names of existing Wiki pages.
307
308 :param prefix: if given, only names that start with that
309 prefix are included.
310 """
311 for page in self.pages:
312 if not prefix or page.startswith(prefix):
313 yield page
314
315 - def has_page(self, pagename):
316 """Whether a page with the specified name exists."""
317 return pagename.rstrip('/') in self.pages
318
321
323 """Resolves a pagename relative to a referrer pagename."""
324 if pagename.startswith(('./', '../')) or pagename in ('.', '..'):
325 return self._resolve_relative_name(pagename, referrer)
326 return pagename
327
328
329
330 XML_NAME = r"[\w:](?<!\d)(?:[\w:.-]*[\w-])?"
331
332
333
334 PAGE_SPLIT_RE = re.compile(r"([a-z])([A-Z])(?=[a-z])")
335
336 Lu = ''.join(unichr(c) for c in range(0, 0x10000) if unichr(c).isupper())
337 Ll = ''.join(unichr(c) for c in range(0, 0x10000) if unichr(c).islower())
338
343
345 """Create a label from a wiki target.
346
347 A trailing fragment and query string is stripped. Then, leading ./,
348 ../ and / elements are stripped, except when this would lead to an
349 empty label. Finally, if `split_page_names` is true, the label
350 is split accordingly.
351 """
352 label = target.split('#', 1)[0].split('?', 1)[0]
353 if not label:
354 return target
355 components = label.split('/')
356 for i, comp in enumerate(components):
357 if comp not in ('', '.', '..'):
358 label = '/'.join(components[i:])
359 break
360 return self.format_page_name(label)
361
363 wiki_page_name = (
364 r"(?:[%(upper)s](?:[%(lower)s])+/?){2,}"
365 r"(?:@[0-9]+)?"
366 r"(?:#%(xml)s)?"
367 r"(?=:(?:\Z|\s)|[^:\w%(upper)s%(lower)s]|\s|\Z)"
368
369 % {'upper': self.Lu, 'lower': self.Ll, 'xml': self.XML_NAME})
370
371
372 def wikipagename_link(formatter, match, fullmatch):
373 return self._format_link(formatter, 'wiki', match,
374 self.format_page_name(match),
375 self.ignore_missing_pages, match)
376
377
378
379 yield (r"!?(?<![\w/])(?:\.?\.?/)*"
380 + wiki_page_name, wikipagename_link)
381
382
383 def wikipagename_with_label_link(formatter, match, fullmatch):
384 page = fullmatch.group('wiki_page')
385 label = fullmatch.group('wiki_label')
386 return self._format_link(formatter, 'wiki', page, label.strip(),
387 self.ignore_missing_pages, match)
388 yield (r"!?\[(?P<wiki_page>%s)\s+(?P<wiki_label>%s|[^\]]+)\]"
389 % (wiki_page_name, WikiParser.QUOTED_STRING),
390 wikipagename_with_label_link)
391
392
393 def internal_free_link(fmt, m, fullmatch):
394 page = fullmatch.group('ifl_page')[1:-1]
395 label = fullmatch.group('ifl_label')
396 if label is None:
397 label = self.make_label_from_target(page)
398 return self._format_link(fmt, 'wiki', page, label.strip(), False)
399 yield (r"!?\[(?P<ifl_page>%s)(?:\s+(?P<ifl_label>%s|[^\]]+))?\]"
400 % (WikiParser.QUOTED_STRING, WikiParser.QUOTED_STRING),
401 internal_free_link)
402
404 def link_resolver(formatter, ns, target, label, fullmatch=None):
405 if fullmatch is not None:
406
407
408
409 groups = fullmatch.groupdict()
410 if groups.get('lns') and not groups.get('label'):
411 label = self.make_label_from_target(target)
412 return self._format_link(formatter, ns, target, label, False)
413 yield ('wiki', link_resolver)
414
453
455 base = referrer.split('/')
456 components = pagename.split('/')
457 for i, comp in enumerate(components):
458 if comp == '..':
459 if base:
460 base.pop()
461 elif comp != '.':
462 base.extend(components[i:])
463 break
464 return '/'.join(base)
465
467 referrer = referrer.split('/')
468 if len(referrer) == 1:
469 return pagename
470
471 for i in range(len(referrer) - 1, 0, -1):
472 name = '/'.join(referrer[:i]) + '/' + pagename
473 if self.has_page(name):
474 return name
475 if self.has_page(pagename):
476 return pagename
477
478
479
480 if '/' in pagename:
481 (first, rest) = pagename.split('/', 1)
482 for (i, part) in enumerate(referrer):
483 if first == part:
484 anchor = '/'.join(referrer[:i + 1])
485 if self.has_page(anchor):
486 return anchor + '/' + rest
487
488 return '/'.join(referrer[:-1]) + '/' + pagename
489
490
491
494
496 """
497 >>> from trac.test import EnvironmentStub
498 >>> from trac.resource import Resource, get_resource_description
499 >>> env = EnvironmentStub()
500 >>> main = Resource('wiki', 'WikiStart')
501 >>> get_resource_description(env, main)
502 'WikiStart'
503
504 >>> get_resource_description(env, main(version=3))
505 'WikiStart'
506
507 >>> get_resource_description(env, main(version=3), format='summary')
508 'WikiStart'
509
510 >>> env.config['wiki'].set('split_page_names', 'true')
511 >>> get_resource_description(env, main(version=3))
512 'Wiki Start'
513 """
514 return self.format_page_name(resource.id)
515
517 """
518 >>> from trac.test import EnvironmentStub
519 >>> from trac.resource import Resource, resource_exists
520 >>> env = EnvironmentStub()
521
522 >>> resource_exists(env, Resource('wiki', 'WikiStart'))
523 False
524
525 >>> from trac.wiki.model import WikiPage
526 >>> main = WikiPage(env, 'WikiStart')
527 >>> main.text = 'some content'
528 >>> main.save('author', 'no comment', '::1')
529 >>> resource_exists(env, main.resource)
530 True
531 """
532 if resource.version is None:
533 return resource.id in self.pages
534 return bool(self.env.db_query(
535 "SELECT name FROM wiki WHERE name=%s AND version=%s",
536 (resource.id, resource.version)))
537