Edgewall Software

root/trunk/trac/wiki/api.py

Revision 8182, 12.7 KB (checked in by rblank, 2 months ago)

0.12dev: Invalidate the wiki page name cache in the model, in the same transaction as the wiki page update, in the same way as the ticket field cache.

Related to #8270.

  • Property svn:eol-style set to native
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2003-2009 Edgewall Software
4# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
5# Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de>
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 http://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 http://trac.edgewall.org/log/.
15#
16# Author: Jonas Borgström <jonas@edgewall.com>
17#         Christopher Lenz <cmlenz@gmx.de>
18
19import urllib
20import re
21from StringIO import StringIO
22
23from genshi.builder import tag
24
25from trac.cache import cached
26from trac.config import BoolOption
27from trac.core import *
28from trac.resource import IResourceManager
29from trac.util.html import html
30from trac.util.translation import _
31from trac.wiki.parser import WikiParser
32
33
34class IWikiChangeListener(Interface):
35    """Extension point interface for components that should get notified about
36    the creation, deletion and modification of wiki pages.
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    def wiki_page_deleted(page):
46        """Called when a page has been deleted."""
47
48    def wiki_page_version_deleted(page):
49        """Called when a version of a page has been deleted."""
50
51
52class IWikiPageManipulator(Interface):
53    """Extension point interface for components that need to do specific
54    pre and post processing of wiki page changes.
55   
56    Unlike change listeners, a manipulator can reject changes being committed
57    to the database.
58    """
59
60    def prepare_wiki_page(req, page, fields):
61        """Not currently called, but should be provided for future
62        compatibility."""
63
64    def validate_wiki_page(req, page):
65        """Validate a wiki page after it's been populated from user input.
66       
67        Must return a list of `(field, message)` tuples, one for each problem
68        detected. `field` can be `None` to indicate an overall problem with the
69        page. Therefore, a return value of `[]` means everything is OK."""
70
71
72class IWikiMacroProvider(Interface):
73    """Extension point interface for components that provide Wiki macros."""
74
75    def get_macros():
76        """Return an iterable that provides the names of the provided macros."""
77
78    def get_macro_description(name):
79        """Return a plain text description of the macro with the specified name.
80        """
81
82    def render_macro(req, name, content):
83        """Return the HTML output of the macro (deprecated)"""
84
85    def expand_macro(formatter, name, content):
86        """Called by the formatter when rendering the parsed wiki text.
87
88        (since 0.11)
89        """
90
91
92class IWikiSyntaxProvider(Interface):
93 
94    def get_wiki_syntax():
95        """Return an iterable that provides additional wiki syntax.
96
97        Additional wiki syntax correspond to a pair of (regexp, cb),
98        the `regexp` for the additional syntax and the callback `cb`
99        which will be called if there's a match.
100        That function is of the form cb(formatter, ns, match).
101        """
102 
103    def get_link_resolvers():
104        """Return an iterable over (namespace, formatter) tuples.
105
106        Each formatter should be a function of the form
107        fmt(formatter, ns, target, label), and should
108        return some HTML fragment.
109        The `label` is already HTML escaped, whereas the `target` is not.
110        """
111
112
113def parse_args(args, strict=True):
114    """Utility for parsing macro "content" and splitting them into arguments.
115
116    The content is split along commas, unless they are escaped with a
117    backquote (like this: \,).
118   
119    :param args: macros arguments, as plain text
120    :param strict: if `True`, only Python-like identifiers will be
121                   recognized as keyword arguments
122
123    Example usage:
124
125    >>> parse_args('')
126    ([], {})
127    >>> parse_args('Some text')
128    (['Some text'], {})
129    >>> parse_args('Some text, mode= 3, some other arg\, with a comma.')
130    (['Some text', ' some other arg, with a comma.'], {'mode': ' 3'})
131    >>> parse_args('milestone=milestone1,status!=closed', strict=False)
132    ([], {'status!': 'closed', 'milestone': 'milestone1'})
133   
134    """   
135    largs, kwargs = [], {}
136    if args:
137        for arg in re.split(r'(?<!\\),', args):
138            arg = arg.replace(r'\,', ',')
139            if strict:
140                m = re.match(r'\s*[a-zA-Z_]\w+=', arg)
141            else:
142                m = re.match(r'\s*[^=]+=', arg)
143            if m:
144                kw = arg[:m.end()-1].strip()
145                if strict:
146                    kw = unicode(kw).encode('utf-8')
147                kwargs[kw] = arg[m.end():]
148            else:
149                largs.append(arg)
150    return largs, kwargs
151
152
153
154class WikiSystem(Component):
155    """Represents the wiki system."""
156
157    implements(IWikiSyntaxProvider, IResourceManager)
158
159    change_listeners = ExtensionPoint(IWikiChangeListener)
160    macro_providers = ExtensionPoint(IWikiMacroProvider)
161    syntax_providers = ExtensionPoint(IWikiSyntaxProvider)
162
163    ignore_missing_pages = BoolOption('wiki', 'ignore_missing_pages', 'false',
164        """Enable/disable highlighting CamelCase links to missing pages
165        (''since 0.9'').""")
166
167    split_page_names = BoolOption('wiki', 'split_page_names', 'false',
168        """Enable/disable splitting the WikiPageNames with space characters
169        (''since 0.10'').""")
170
171    render_unsafe_content = BoolOption('wiki', 'render_unsafe_content', 'false',
172        """Enable/disable the use of unsafe HTML tags such as `<script>` or
173        `<embed>` with the HTML [wiki:WikiProcessors WikiProcessor]
174        (''since 0.10.4'').
175
176        For public sites where anonymous users can edit the wiki it is
177        recommended to leave this option disabled (which is the default).""")
178
179    @cached
180    def pages(self, db):
181        """Return the names of all existing wiki pages."""
182        cursor = db.cursor()
183        cursor.execute("SELECT DISTINCT name FROM wiki")
184        return [name for (name,) in cursor]
185
186    # Public API
187
188    def get_pages(self, prefix=None):
189        """Iterate over the names of existing Wiki pages.
190
191        If the `prefix` parameter is given, only names that start with that
192        prefix are included.
193        """
194        for page in self.pages.get():
195            if not prefix or page.startswith(prefix):
196                yield page
197
198    def has_page(self, pagename):
199        """Whether a page with the specified name exists."""
200        return pagename.rstrip('/') in self.pages.get()
201
202    # IWikiSyntaxProvider methods
203
204    XML_NAME = r"[\w:](?<!\d)(?:[\w:.-]*[\w-])?"
205    # See http://www.w3.org/TR/REC-xml/#id,
206    # here adapted to exclude terminal "." and ":" characters
207
208    PAGE_SPLIT_RE = re.compile(r"([a-z])([A-Z])(?=[a-z])")
209   
210    def format_page_name(self, page, split=False):
211        if split or self.split_page_names:
212            return self.PAGE_SPLIT_RE.sub(r"\1 \2", page)
213        return page
214
215    def get_wiki_syntax(self):
216        from trac.wiki.formatter import Formatter
217        lower = r'(?<![A-Z0-9_])' # No Upper case when looking behind
218        upper = r'(?<![a-z0-9_])' # No Lower case when looking behind
219        wiki_page_name = (
220            r"\w%s(?:\w%s)+(?:\w%s(?:\w%s)*[\w/]%s)+" % # wiki words
221            (upper, lower, upper, lower, lower) +
222            r"(?:@\d+)?" # optional version
223            r"(?:#%s)?" % self.XML_NAME + # optional fragment id
224            r"(?=:(?:\Z|\s)|[^:a-zA-Z]|\s|\Z)" # what should follow it
225            )
226
227       
228        # Regular WikiPageNames
229        def wikipagename_link(formatter, match, fullmatch):
230            if not _check_unicode_camelcase(match):
231                return match
232            return self._format_link(formatter, 'wiki', match,
233                                     self.format_page_name(match),
234                                     self.ignore_missing_pages, match)
235       
236        yield (r"!?(?<!/)\b" + # start at a word boundary but not after '/'
237               wiki_page_name, wikipagename_link)
238
239        # [WikiPageNames with label]
240        def wikipagename_with_label_link(formatter, match, fullmatch):
241            page, label = match[1:-1].split(' ', 1)
242            if not _check_unicode_camelcase(page):
243                return label
244            return self._format_link(formatter, 'wiki', page, label.strip(),
245                                     self.ignore_missing_pages, match)
246        yield (r"!?\[%s\s+(?:%s|[^\]]+)\]" % (wiki_page_name,
247                                              WikiParser.QUOTED_STRING),
248               wikipagename_with_label_link)
249
250        # MoinMoin's ["internal free link"]
251        def internal_free_link(fmt, m, fullmatch): 
252            return self._format_link(fmt, 'wiki', m[2:-2], m[2:-2], False) 
253        yield (r"!?\[(?:%s)\]" % WikiParser.QUOTED_STRING, internal_free_link) 
254
255    def get_link_resolvers(self):
256        def link_resolver(formatter, ns, target, label):
257            return self._format_link(formatter, ns, target, label, False)
258        yield ('wiki', link_resolver)
259
260    def _format_link(self, formatter, ns, pagename, label, ignore_missing,
261                     original_label=None):
262        pagename, query, fragment = formatter.split_link(pagename)
263        version = None
264        if '@' in pagename:
265            pagename, version = pagename.split('@', 1)
266        if version and query:
267            query = '&' + query[1:]
268        pagename = pagename.rstrip('/') or 'WikiStart'
269        if formatter.resource and formatter.resource.realm == 'wiki' \
270                              and not pagename.startswith('/'):
271            prefix = formatter.resource.id
272            if '/' in prefix:
273                while '/' in prefix:
274                    prefix = prefix.rsplit('/', 1)[0]
275                    name = prefix + '/' + pagename
276                    if self.has_page(name):
277                        pagename = name
278                        break
279                else:
280                    if not self.has_page(pagename):
281                        pagename = formatter.resource.id.rsplit('/', 1)[0] \
282                                   + '/' + pagename
283        pagename = pagename.lstrip('/')
284        if 'WIKI_VIEW' in formatter.perm('wiki', pagename, version):
285            href = formatter.href.wiki(pagename, version=version) + query \
286                   + fragment
287            if self.has_page(pagename):
288                return tag.a(label, href=href, class_='wiki')
289            else:
290                if ignore_missing:
291                    return original_label or label
292                if 'WIKI_CREATE' in formatter.perm('wiki', pagename, version):
293                    return tag.a(label + '?', class_='missing wiki',
294                                 href=href, rel='nofollow')
295                else:
296                    return tag.a(label + '?', class_='missing wiki')
297        elif ignore_missing and not self.has_page(pagename):
298            return label
299        else:
300            return tag.a(label, class_='forbidden wiki',
301                         title=_("no permission to view this wiki page"))
302
303    # IResourceManager methods
304
305    def get_resource_realms(self):
306        yield 'wiki'
307
308    def get_resource_description(self, resource, format, **kwargs):
309        """
310        >>> from trac.test import EnvironmentStub
311        >>> from trac.resource import Resource, get_resource_description
312        >>> env = EnvironmentStub()
313        >>> main = Resource('wiki', 'WikiStart')
314        >>> get_resource_description(env, main)
315        'WikiStart'
316
317        >>> get_resource_description(env, main(version=3))
318        'WikiStart'
319
320        >>> get_resource_description(env, main(version=3), format='summary')
321        'WikiStart'
322
323        >>> env.config['wiki'].set('split_page_names', 'true')
324        >>> get_resource_description(env, main(version=3))
325        'Wiki Start'
326        """
327        return self.format_page_name(resource.id)
328
329
330def _check_unicode_camelcase(pagename):
331    """A camelcase word must have at least 2 humps (well...)
332
333    >>> _check_unicode_camelcase(u"\xc9l\xe9phant")
334    False
335    >>> _check_unicode_camelcase(u"\xc9l\xe9Phant")
336    True
337    >>> _check_unicode_camelcase(u"\xe9l\xe9Phant")
338    False
339    >>> _check_unicode_camelcase(u"\xc9l\xe9PhanT")
340    False
341    """
342    if not pagename[0].isupper():
343        return False
344    pagename = pagename.split('@', 1)[0].split('#', 1)[0]
345    if not pagename[-1].islower():
346        return False
347    humps = 0
348    for i in xrange(1, len(pagename)):
349        if pagename[i-1].isupper():
350            if pagename[i].islower():
351                humps += 1
352            else:
353                return False
354    return humps > 1
355
Note: See TracBrowser for help on using the browser.