Package trac :: Package wiki :: Module api

Source Code for Module trac.wiki.api

  1  # -*- coding: utf-8 -*- 
  2  # 
  3  # Copyright (C) 2003-2020 Edgewall Software 
  4  # Copyright (C) 2003-2005 Jonas Borgström <[email protected]> 
  5  # Copyright (C) 2004-2005 Christopher Lenz <[email protected]> 
  6  # All rights reserved. 
  7  # 
  8  # This software is licensed as described in the file COPYING, which 
  9  # you should have received as part of this distribution. The terms 
 10  # are also available at https://trac.edgewall.org/wiki/TracLicense. 
 11  # 
 12  # This software consists of voluntary contributions made by many 
 13  # individuals. For the exact contribution history, see the revision 
 14  # history and logs, available at https://trac.edgewall.org/log/. 
 15  # 
 16  # Author: Jonas Borgström <[email protected]> 
 17  #         Christopher Lenz <[email protected]> 
 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 
31 32 33 -class IWikiChangeListener(Interface):
34 """Components that want to get notified about the creation, 35 deletion and modification of wiki pages should implement that 36 interface. 37 """ 38
39 - def wiki_page_added(page):
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
49 - def wiki_page_deleted(page):
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
58 - def wiki_page_comment_modified(page, old_comment):
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
92 93 -class IWikiMacroProvider(Interface):
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
100 - def get_macros():
101 """Return an iterable that provides the names of the provided macros. 102 """
103
104 - def get_macro_description(name):
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
114 - def is_inline(content):
115 """Return `True` if the content generated is an inline XHTML element. 116 117 .. versionadded :: 1.0 118 """
119
120 - def expand_macro(formatter, name, content, args=None):
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
178 179 -class IWikiSyntaxProvider(Interface):
180 """Enrich the Wiki syntax with new markup.""" 181
182 - def get_wiki_syntax():
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
204
205 -def parse_args(args, strict=True):
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
253 254 -class WikiSystem(Component):
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
298 - def pages(self):
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 # Public API 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
319 - def is_safe_origin(self, uri, req=None):
320 return is_safe_origin(self.safe_origins, uri, req=req)
321
322 - def resolve_relative_name(self, pagename, referrer):
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 # IWikiSyntaxProvider methods 329 330 XML_NAME = r"[\w:](?<!\d)(?:[\w:.-]*[\w-])?" 331 # See http://www.w3.org/TR/REC-xml/#id, 332 # here adapted to exclude terminal "." and ":" characters 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
339 - def format_page_name(self, page, split=False):
340 if split or self.split_page_names: 341 return self.PAGE_SPLIT_RE.sub(r"\1 \2", page) 342 return page
343
344 - def make_label_from_target(self, target):
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
362 - def get_wiki_syntax(self):
363 wiki_page_name = ( 364 r"(?:[%(upper)s](?:[%(lower)s])+/?){2,}" # wiki words 365 r"(?:@[0-9]+)?" # optional version 366 r"(?:#%(xml)s)?" # optional fragment id 367 r"(?=:(?:\Z|\s)|[^:\w%(upper)s%(lower)s]|\s|\Z)" 368 # what should follow it 369 % {'upper': self.Lu, 'lower': self.Ll, 'xml': self.XML_NAME}) 370 371 # Regular WikiPageNames 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 # Start after any non-word char except '/', with optional relative or 378 # absolute prefix 379 yield (r"!?(?<![\w/])(?:\.?\.?/)*" 380 + wiki_page_name, wikipagename_link) 381 382 # [WikiPageNames with label] 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 # MoinMoin's ["internal free link"] and ["free link" with label] 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 413 yield ('wiki', link_resolver) 414 453
454 - def _resolve_relative_name(self, pagename, referrer):
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
466 - def _resolve_scoped_name(self, pagename, referrer):
467 referrer = referrer.split('/') 468 if len(referrer) == 1: # Non-hierarchical referrer 469 return pagename 470 # Test for pages with same name, higher in the hierarchy 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 # If we are on First/Second/Third, and pagename is Second/Other, 478 # resolve to First/Second/Other instead of First/Second/Second/Other 479 # See https://trac.edgewall.org/ticket/4507#comment:12 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 # Assume the user wants a sibling of referrer 488 return '/'.join(referrer[:-1]) + '/' + pagename
489 490 # IResourceManager methods 491
492 - def get_resource_realms(self):
493 yield self.realm
494
495 - def get_resource_description(self, resource, format, **kwargs):
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
516 - def resource_exists(self, resource):
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