~ubuntu-branches/ubuntu/natty/moin/natty-updates

« back to all changes in this revision

Viewing changes to MoinMoin/support/htmlmarkup.py

  • Committer: Bazaar Package Importer
  • Author(s): Jonas Smedegaard
  • Date: 2008-06-22 21:17:13 UTC
  • mfrom: (0.9.1 upstream)
  • Revision ID: james.westby@ubuntu.com-20080622211713-fpo2zrq3s5dfecxg
Tags: 1.7.0-3
Simplify /etc/moin/wikilist format: "USER URL" (drop unneeded middle
CONFIG_DIR that was wrongly advertised as DATA_DIR).  Make
moin-mass-migrate handle both formats and warn about deprecation of
the old one.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
# copied from trac.util.html, revision 3609, merged on 2006-08-20
 
3
#
 
4
# Copyright (C) 2003-2006 Edgewall Software
 
5
# Copyright 2006 MoinMoin:AlexanderSchremmer
 
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.com/license.html.
 
11
#
 
12
# This software consists of voluntary contributions made by many
 
13
# individuals. For exact contribution history, see the revision
 
14
# history and logs, available at http://projects.edgewall.com/trac/.
 
15
 
 
16
import htmlentitydefs
 
17
from HTMLParser import HTMLParser, HTMLParseError
 
18
import re
 
19
try:
 
20
    frozenset
 
21
except NameError:
 
22
    from sets import ImmutableSet as frozenset
 
23
from StringIO import StringIO
 
24
 
 
25
__all__ = ['escape', 'unescape', 'html']
 
26
 
 
27
_EMPTY_TAGS = frozenset(['br', 'hr', 'img', 'input'])
 
28
_BOOLEAN_ATTRS = frozenset(['selected', 'checked', 'compact', 'declare',
 
29
                            'defer', 'disabled', 'ismap', 'multiple', 'nohref',
 
30
                            'noresize', 'noshade', 'nowrap'])
 
31
 
 
32
 
 
33
class Markup(unicode):
 
34
    """Marks a string as being safe for inclusion in XML output without needing
 
35
    to be escaped.
 
36
    
 
37
    Strings are normally automatically escaped when added to the HDF.
 
38
    `Markup`-strings are however an exception. Use with care.
 
39
    
 
40
    (since Trac 0.9.3)
 
41
    """
 
42
    def __new__(self, text='', *args):
 
43
        if args:
 
44
            text %= tuple([escape(arg) for arg in args])
 
45
        return unicode.__new__(self, text)
 
46
 
 
47
    def __add__(self, other):
 
48
        return Markup(unicode(self) + Markup.escape(other))
 
49
 
 
50
    def __mod__(self, args):
 
51
        if not isinstance(args, (list, tuple)):
 
52
            args = [args]
 
53
        return Markup(unicode.__mod__(self,
 
54
                                      tuple([escape(arg) for arg in args])))
 
55
 
 
56
    def __mul__(self, num):
 
57
        return Markup(unicode(self) * num)
 
58
 
 
59
    def join(self, seq):
 
60
        return Markup(unicode(self).join([Markup.escape(item) for item in seq]))
 
61
 
 
62
    def stripentities(self, keepxmlentities=False):
 
63
        """Return a copy of the text with any character or numeric entities
 
64
        replaced by the equivalent UTF-8 characters.
 
65
        
 
66
        If the `keepxmlentities` parameter is provided and evaluates to `True`,
 
67
        the core XML entities (&, ', >, < and ").
 
68
        
 
69
        (Since Trac 0.10)
 
70
        """
 
71
        def _replace_entity(match):
 
72
            if match.group(1): # numeric entity
 
73
                ref = match.group(1)
 
74
                if ref.startswith('x'):
 
75
                    ref = int(ref[1:], 16)
 
76
                else:
 
77
                    ref = int(ref, 10)
 
78
                return unichr(ref)
 
79
            else: # character entity
 
80
                ref = match.group(2)
 
81
                if keepxmlentities and ref in ('amp', 'apos', 'gt', 'lt', 'quot'):
 
82
                    return '&%s;' % ref
 
83
                try:
 
84
                    codepoint = htmlentitydefs.name2codepoint[ref]
 
85
                    return unichr(codepoint)
 
86
                except KeyError:
 
87
                    if keepxmlentities:
 
88
                        return '&%s;' % ref
 
89
                    else:
 
90
                        return ref
 
91
        return Markup(re.sub(r'&(?:#((?:\d+)|(?:[xX][0-9a-fA-F]+));?|(\w+);)',
 
92
                             _replace_entity, self))
 
93
 
 
94
    def striptags(self):
 
95
        """Return a copy of the text with all XML/HTML tags removed."""
 
96
        return Markup(re.sub(r'<[^>]*?>', '', self))
 
97
 
 
98
    def escape(cls, text, quotes=True):
 
99
        """Create a Markup instance from a string and escape special characters
 
100
        it may contain (<, >, & and \").
 
101
        
 
102
        If the `quotes` parameter is set to `False`, the \" character is left
 
103
        as is. Escaping quotes is generally only required for strings that are
 
104
        to be used in attribute values.
 
105
        """
 
106
        if isinstance(text, (cls, Element)):
 
107
            return text
 
108
        text = unicode(text)
 
109
        if not text:
 
110
            return cls()
 
111
        text = text.replace('&', '&amp;') \
 
112
                   .replace('<', '&lt;') \
 
113
                   .replace('>', '&gt;')
 
114
        if quotes:
 
115
            text = text.replace('"', '&#34;')
 
116
        return cls(text)
 
117
    escape = classmethod(escape)
 
118
 
 
119
    def unescape(self):
 
120
        """Reverse-escapes &, <, > and \" and returns a `unicode` object."""
 
121
        if not self:
 
122
            return ''
 
123
        return unicode(self).replace('&#34;', '"') \
 
124
                            .replace('&gt;', '>') \
 
125
                            .replace('&lt;', '<') \
 
126
                            .replace('&amp;', '&')
 
127
 
 
128
    def plaintext(self, keeplinebreaks=True):
 
129
        """Returns the text as a `unicode`with all entities and tags removed."""
 
130
        text = unicode(self.striptags().stripentities())
 
131
        if not keeplinebreaks:
 
132
            text = text.replace('\n', ' ')
 
133
        return text
 
134
 
 
135
    def sanitize(self):
 
136
        """Parse the text as HTML and return a cleaned up XHTML representation.
 
137
        
 
138
        This will remove any javascript code or other potentially dangerous
 
139
        elements.
 
140
        
 
141
        If the HTML cannot be parsed, an `HTMLParseError` will be raised by the
 
142
        underlying `HTMLParser` module, which should be handled by the caller of
 
143
        this function.
 
144
        """
 
145
        buf = StringIO()
 
146
        sanitizer = HTMLSanitizer(buf)
 
147
        sanitizer.feed(self.stripentities(keepxmlentities=True))
 
148
        return Markup(buf.getvalue())
 
149
 
 
150
 
 
151
escape = Markup.escape
 
152
 
 
153
def unescape(text):
 
154
    """Reverse-escapes &, <, > and \" and returns a `unicode` object."""
 
155
    if not isinstance(text, Markup):
 
156
        return text
 
157
    return text.unescape()
 
158
 
 
159
 
 
160
class Deuglifier(object):
 
161
 
 
162
    def __new__(cls):
 
163
        self = object.__new__(cls)
 
164
        if not hasattr(cls, '_compiled_rules'):
 
165
            cls._compiled_rules = re.compile('(?:' + '|'.join(cls.rules()) + ')')
 
166
        self._compiled_rules = cls._compiled_rules
 
167
        return self
 
168
    
 
169
    def format(self, indata):
 
170
        return re.sub(self._compiled_rules, self.replace, indata)
 
171
 
 
172
    def replace(self, fullmatch):
 
173
        for mtype, match in fullmatch.groupdict().items():
 
174
            if match:
 
175
                if mtype == 'font':
 
176
                    return '<span>'
 
177
                elif mtype == 'endfont':
 
178
                    return '</span>'
 
179
                return '<span class="code-%s">' % mtype
 
180
 
 
181
 
 
182
class HTMLSanitizer(HTMLParser):
 
183
 
 
184
    safe_tags = frozenset(['a', 'abbr', 'acronym', 'address', 'area',
 
185
        'b', 'big', 'blockquote', 'br', 'button', 'caption', 'center',
 
186
        'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dir',
 
187
        'div', 'dl', 'dt', 'em', 'fieldset', 'font', 'form', 'h1', 'h2',
 
188
        'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input', 'ins', 'kbd',
 
189
        'label', 'legend', 'li', 'map', 'menu', 'ol', 'optgroup',
 
190
        'option', 'p', 'pre', 'q', 's', 'samp', 'select', 'small',
 
191
        'span', 'strike', 'strong', 'sub', 'sup', 'table', 'tbody',
 
192
        'td', 'textarea', 'tfoot', 'th', 'thead', 'tr', 'tt', 'u', 'ul',
 
193
        'var'])
 
194
    safe_attrs = frozenset(['abbr', 'accept', 'accept-charset',
 
195
        'accesskey', 'action', 'align', 'alt', 'axis', 'border', 'bgcolor',
 
196
        'cellpadding', 'cellspacing', 'char', 'charoff', 'charset',
 
197
        'checked', 'cite', 'class', 'clear', 'cols', 'colspan', 'color',
 
198
        'compact', 'coords', 'datetime', 'dir', 'disabled', 'enctype',
 
199
        'for', 'frame', 'headers', 'height', 'href', 'hreflang',
 
200
        'hspace', 'id', 'ismap', 'label', 'lang', 'longdesc',
 
201
        'maxlength', 'media', 'method', 'multiple', 'name', 'nohref',
 
202
        'noshade', 'nowrap', 'prompt', 'readonly', 'rel', 'rev', 'rows',
 
203
        'rowspan', 'rules', 'scope', 'selected', 'shape', 'size',
 
204
        'span', 'src', 'start', 'style', 'summary', 'tabindex',
 
205
        'target', 'title', 'type', 'usemap', 'valign', 'value',
 
206
        'vspace', 'width'])
 
207
    ignore_tags = frozenset(['html', 'body'])
 
208
    
 
209
    uri_attrs = frozenset(['action', 'background', 'dynsrc', 'href',
 
210
                           'lowsrc', 'src'])
 
211
    safe_schemes = frozenset(['file', 'ftp', 'http', 'https', 'mailto',
 
212
                              None])
 
213
 
 
214
    def __init__(self, out):
 
215
        HTMLParser.__init__(self)
 
216
        self.out = out
 
217
        self.waiting_for = None
 
218
 
 
219
    def handle_starttag(self, tag, attrs):
 
220
        if self.waiting_for:
 
221
            return
 
222
        if tag in self.ignore_tags:
 
223
            return
 
224
        
 
225
        if tag not in self.safe_tags:
 
226
            self.waiting_for = tag
 
227
            return
 
228
        self.out.write('<' + tag)
 
229
 
 
230
        def _get_scheme(text):
 
231
            if ':' not in text:
 
232
                return None
 
233
            chars = [char for char in text.split(':', 1)[0]
 
234
                     if char.isalnum()]
 
235
            return ''.join(chars).lower()
 
236
 
 
237
        for attrname, attrval in attrs:
 
238
            if attrname not in self.safe_attrs:
 
239
                continue
 
240
            elif attrname in self.uri_attrs:
 
241
                # Don't allow URI schemes such as "javascript:"
 
242
                if _get_scheme(attrval) not in self.safe_schemes:
 
243
                    continue
 
244
            elif attrname == 'style':
 
245
                # Remove dangerous CSS declarations from inline styles
 
246
                decls = []
 
247
                for decl in filter(None, attrval.split(';')):
 
248
                    is_evil = False
 
249
                    if 'expression' in decl:
 
250
                        is_evil = True
 
251
                    for m in re.finditer(r'url\s*\(([^)]+)', decl):
 
252
                        if _get_scheme(m.group(1)) not in self.safe_schemes:
 
253
                            is_evil = True
 
254
                            break
 
255
                    if not is_evil:
 
256
                        decls.append(decl.strip())
 
257
                if not decls:
 
258
                    continue
 
259
                attrval = '; '.join(decls)
 
260
            self.out.write(' ' + attrname + '="' + escape(attrval) + '"')
 
261
 
 
262
        if tag in _EMPTY_TAGS:
 
263
            self.out.write(' />')
 
264
        else:
 
265
            self.out.write('>')
 
266
 
 
267
    def handle_entityref(self, name):
 
268
        if not self.waiting_for:
 
269
            self.out.write('&%s;' % name)
 
270
 
 
271
    def handle_data(self, data):
 
272
        if not self.waiting_for:
 
273
            self.out.write(escape(data, quotes=False))
 
274
 
 
275
    def handle_endtag(self, tag):
 
276
        if tag in self.ignore_tags:
 
277
            return
 
278
 
 
279
        if self.waiting_for:
 
280
            if self.waiting_for == tag:
 
281
                self.waiting_for = None
 
282
            return
 
283
        if tag not in _EMPTY_TAGS:
 
284
            self.out.write('</' + tag + '>')
 
285
 
 
286
 
 
287
class Fragment(object):
 
288
    __slots__ = ['children']
 
289
 
 
290
    def __init__(self):
 
291
        self.children = []
 
292
 
 
293
    def append(self, node):
 
294
        """Append an element or string as child node."""
 
295
        if isinstance(node, (Element, Markup, basestring, int, float, long)):
 
296
            # For objects of a known/primitive type, we avoid the check for
 
297
            # whether it is iterable for better performance
 
298
            self.children.append(node)
 
299
        elif isinstance(node, Fragment):
 
300
            self.children += node.children
 
301
        elif node is not None:
 
302
            try:
 
303
                for child in node:
 
304
                    self.append(child)
 
305
            except TypeError:
 
306
                self.children.append(node)
 
307
 
 
308
    def __call__(self, *args):
 
309
        for arg in args:
 
310
            self.append(arg)
 
311
        return self
 
312
 
 
313
    def serialize(self):
 
314
        """Generator that yield tags and text nodes as strings."""
 
315
        for child in self.children:
 
316
            if isinstance(child, Fragment):
 
317
                yield unicode(child)
 
318
            else:
 
319
                yield escape(child, quotes=False)
 
320
 
 
321
    def __unicode__(self):
 
322
        return u''.join(self.serialize())
 
323
 
 
324
    def __str__(self):
 
325
        return ''.join(self.serialize())
 
326
 
 
327
    def __add__(self, other):
 
328
        return Fragment()(self, other)
 
329
 
 
330
 
 
331
class Element(Fragment):
 
332
    """Simple XHTML output generator based on the builder pattern.
 
333
    
 
334
    Construct XHTML elements by passing the tag name to the constructor:
 
335
    
 
336
    >>> print Element('strong')
 
337
    <strong></strong>
 
338
    
 
339
    Attributes can be specified using keyword arguments. The values of the
 
340
    arguments will be converted to strings and any special XML characters
 
341
    escaped:
 
342
    
 
343
    >>> print Element('textarea', rows=10, cols=60)
 
344
    <textarea rows="10" cols="60"></textarea>
 
345
    >>> print Element('span', title='1 < 2')
 
346
    <span title="1 &lt; 2"></span>
 
347
    >>> print Element('span', title='"baz"')
 
348
    <span title="&#34;baz&#34;"></span>
 
349
    
 
350
    The " character is escaped using a numerical entity.
 
351
    The order in which attributes are rendered is undefined.
 
352
    
 
353
    If an attribute value evaluates to `None`, that attribute is not included
 
354
    in the output:
 
355
    
 
356
    >>> print Element('a', name=None)
 
357
    <a></a>
 
358
    
 
359
    Attribute names that conflict with Python keywords can be specified by
 
360
    appending an underscore:
 
361
    
 
362
    >>> print Element('div', class_='warning')
 
363
    <div class="warning"></div>
 
364
    
 
365
    While the tag names and attributes are not restricted to the XHTML language,
 
366
    some HTML characteristics such as boolean (minimized) attributes and empty
 
367
    elements get special treatment.
 
368
    
 
369
    For compatibility with HTML user agents, some XHTML elements need to be
 
370
    closed using a separate closing tag even if they are empty. For this, the
 
371
    close tag is only ommitted for a small set of elements which are known be
 
372
    be safe for use as empty elements:
 
373
    
 
374
    >>> print Element('br')
 
375
    <br />
 
376
    
 
377
    Trying to add nested elements to such an element will cause an
 
378
    `AssertionError`:
 
379
    
 
380
    >>> Element('br')('Oops')
 
381
    Traceback (most recent call last):
 
382
        ...
 
383
    AssertionError: 'br' elements must not have content
 
384
    
 
385
    Furthermore, boolean attributes such as "selected" or "checked" are omitted
 
386
    if the value evaluates to `False`. Otherwise, the name of the attribute is
 
387
    used for the value:
 
388
    
 
389
    >>> print Element('option', value=0, selected=False)
 
390
    <option value="0"></option>
 
391
    >>> print Element('option', selected='yeah')
 
392
    <option selected="selected"></option>
 
393
    
 
394
    
 
395
    Nested elements can be added to an element by calling the instance using
 
396
    positional arguments. The same technique can also be used for adding
 
397
    attributes using keyword arguments, as one would do in the constructor:
 
398
    
 
399
    >>> print Element('ul')(Element('li'), Element('li'))
 
400
    <ul><li></li><li></li></ul>
 
401
    >>> print Element('a')('Label')
 
402
    <a>Label</a>
 
403
    >>> print Element('a')('Label', href="target")
 
404
    <a href="target">Label</a>
 
405
 
 
406
    Text nodes can be nested in an element by adding strings instead of
 
407
    elements. Any special characters in the strings are escaped automatically:
 
408
 
 
409
    >>> print Element('em')('Hello world')
 
410
    <em>Hello world</em>
 
411
    >>> print Element('em')(42)
 
412
    <em>42</em>
 
413
    >>> print Element('em')('1 < 2')
 
414
    <em>1 &lt; 2</em>
 
415
 
 
416
    This technique also allows mixed content:
 
417
 
 
418
    >>> print Element('p')('Hello ', Element('b')('world'))
 
419
    <p>Hello <b>world</b></p>
 
420
 
 
421
    Elements can also be combined with other elements or strings using the
 
422
    addition operator, which results in a `Fragment` object that contains the
 
423
    operands:
 
424
    
 
425
    >>> print Element('br') + 'some text' + Element('br')
 
426
    <br />some text<br />
 
427
    """
 
428
    __slots__ = ['tagname', 'attr']
 
429
 
 
430
    def __init__(self, tagname_=None, **attr):
 
431
        Fragment.__init__(self)
 
432
        if tagname_:
 
433
            self.tagname = tagname_
 
434
        self.attr = {}
 
435
        self(**attr)
 
436
 
 
437
    def __call__(self, *args, **attr):
 
438
        self.attr.update(attr)
 
439
        return Fragment.__call__(self, *args)
 
440
 
 
441
    def append(self, node):
 
442
        """Append an element or string as child node."""
 
443
        assert self.tagname not in _EMPTY_TAGS, \
 
444
            "'%s' elements must not have content" % self.tagname
 
445
        Fragment.append(self, node)
 
446
 
 
447
    def serialize(self):
 
448
        """Generator that yield tags and text nodes as strings."""
 
449
        starttag = ['<', self.tagname]
 
450
        for name, value in self.attr.items():
 
451
            if value is None:
 
452
                continue
 
453
            if name in _BOOLEAN_ATTRS:
 
454
                if not value:
 
455
                    continue
 
456
                value = name
 
457
            else:
 
458
                name = name.rstrip('_').replace('_', '-')
 
459
            starttag.append(' %s="%s"' % (name.lower(), escape(value)))
 
460
 
 
461
        if self.children or self.tagname not in _EMPTY_TAGS:
 
462
            starttag.append('>')
 
463
            yield Markup(''.join(starttag))
 
464
            for part in Fragment.serialize(self):
 
465
                yield part
 
466
            yield Markup('</%s>', self.tagname)
 
467
 
 
468
        else:
 
469
            starttag.append(' />')
 
470
            yield Markup(''.join(starttag))
 
471
 
 
472
 
 
473
class Tags(object):
 
474
 
 
475
    def __getattribute__(self, name):
 
476
        return Element(name.lower())
 
477
 
 
478
 
 
479
html = Tags()