Mike Orr avatar Mike Orr committed 527e244

Use MarkupSafe package for ``literal`` and ``escape`` in HTML Builder.

Comments (0)

Files changed (9)

 1.2 (unreleased)
 -------------------------
 
-No changes yet.
+* WebHelpers now depends on MarkupSafe. ``literal`` and ``escape`` now use it.
+  
+* webhelpers.html.builder:
+
+  - ``literal`` and ``escape`` now use MarkupSafe, which has a C speedup for
+    escaping, escapes single-quotes for security, and adds new methods to
+    ``literal``. Compatibility should not be a problem; but see the docs if
+    you encounter any edge cases.
 
 1.1 (2010-08-09)
 ----------------

docs/modules/html/builder.rst

 
 .. currentmodule:: webhelpers.html.builder
 
-Other functions
----------------
+Classes
+-------
+
+.. autoclass:: literal(s, encoding=None, errors=strict')
+   :members:
+
+   .. automethod:: escape
+
+      Same as the ``escape`` function but return the proper subclass
+      in subclasses.
+
+   .. automethod:: unescape
+
+   .. automethod:: striptags
+
+.. class:: HTML
+
+   Described above.
+
+Functions
+---------
 
 .. autofunction:: lit_sub
 
     Urlencode the path portion of a URL. This is the same function as
     ``urllib.quote`` in the Python standard library. It's exported here
     with a name that's easier to remember.
+
+The ``markupsafe`` package has a function ``soft_unicode`` which converts a
+string to Unicode if it's not already. Unlike the Python builtin ``unicode()``,
+it will not convert ``Markup`` (``literal``) to plain Unicode, to avoid
+overescaping. This is not included in WebHelpers but you may find it useful.

docs/whats_new.rst

 in boldface;** these may require modifying your application.  See `Changelog
 <changelog.html>`_ for the full changelog.
 
+Version 1.2
+-----------
+
+*webhelpers.html*: The HTML builder now uses Armin Ronacher's
+"MarkupSafe" package, which Mako and Pylons have also switched to.  MarkupSafe
+has a C speedup for escaping, escapes single-quotes for greater security, and
+adds new methods to ``literal``.
+
+* **literal** is now a subclass of ``markupsafe.Markup``
+
+* **escape** is ``markupsafe.escape_silent``
+
+*Note*: ``escape_silent`` does not exist yet in MarkupSafe 0.9.3, but
+WebHelpers has a fallback. 
+
 Version 1.1
 -----------
 
-*webhelpers.pylonslib.minify*
-
-    **The ``_jsmin`` module was removed due to a licensing issue.** (Fedora
-    could not distribute it due to a non-free clause in the license.) **To
-    minify Javascript, you must install the external "jsmin" package from
-    PyPI.** Otherwise the helper will pass Javascript through unchanged
-    and issue a warning. CSS minification is not affected.
-
+*webhelpers.pylonslib.minify*: The Javascript minification code was removed
+due to a non-free license. **The helper now minifies Javascript only if the
+"jsmin" package is installed.**  Otherwise it issues a warning and leaves the
+Javascript unchanged. CSS minification is not affected. Details are in
+webhelpers/pylonslib/_minify.py .
 
 Version 1.0
 -----------
     zip_safe=False,
     include_package_data=True,
     install_requires=[
+        'MarkupSafe>=0.9.2',
         ],
     tests_require=[ 
-      'nose',
-      'routes'
+      'Nose',
+      'Routes'
+      'WebOb',
       ], 
     test_suite='nose.collector',
     classifiers=["Development Status :: 4 - Beta",

tests/test_html.py

 
 def test_double_escape():
     quoted = escape(u'This string is "quoted"')
-    assert quoted == u'This string is &quot;quoted&quot;'
+    eq_(quoted, u'This string is &#34;quoted&#34;')
     dbl_quoted = escape(quoted)
-    assert quoted == dbl_quoted
+    eq_(quoted, dbl_quoted)
 
 def test_literal():
     lit = literal(u'This string <>')
     other = literal(u'<other>')
-    assert u'This string <><other>' == lit + other
+    eq_(u'This string <><other>', lit + other)
     assert type(lit + other) is literal
     
-    assert u'&quot;<other>' == '"' + other
-    assert u'<other>&quot;' == other + '"'
+    eq_(u'&#34;<other>', '"' + other)
+    eq_(u'<other>&#34;', other + '"')
     
     mod = literal('<%s>ello')
-    assert u'<&lt;H&gt;>ello' == mod % '<H>'
+    eq_(u'<&lt;H&gt;>ello', mod % '<H>')
     assert type(mod % '<H>') is literal
-    assert HTML('<a>') == '&lt;a&gt;'
+    eq_(HTML('<a>'), '&lt;a&gt;')
     assert type(HTML('<a>')) is literal
 
 def test_literal_dict():
     lit = literal(u'This string <>')
     unq = 'This has <crap>'
     sub = literal('%s and %s')
-    assert u'This string <> and This has &lt;crap&gt;' == sub % (lit, unq)
+    eq_(u'This string <> and This has &lt;crap&gt;', sub % (lit, unq))
     
     sub = literal('%(lit)s and %(lit)r')
-    assert u"This string <> and literal(u'This string &lt;&gt;')" == sub % dict(lit=lit)
+    eq_(u"This string <> and literal(u&#39;This string &lt;&gt;&#39;)", sub % dict(lit=lit))
     sub = literal('%(unq)r and %(unq)s')
-    assert u"'This has &lt;crap&gt;' and This has &lt;crap&gt;" == sub % dict(unq=unq)
+    eq_(u"&#39;This has &lt;crap&gt;&#39; and This has &lt;crap&gt;", sub % dict(unq=unq))
 
 def test_literal_mul():
     lit = literal(u'<>')
-    assert u'<><><>' == lit * 3
+    eq_(u'<><><>', lit * 3)
     assert isinstance(lit*3, literal)
 
 def test_literal_join():
     lit = literal(u'<>')
     assert isinstance(lit.join(['f', 'a']), literal)
-    assert u'f<>a' == lit.join(('f', 'a'))
+    eq_(u'f<>a', lit.join(('f', 'a')))
 
 def test_literal_int():
     lit = literal(u'<%i>')
-    assert u'<5>' == lit % 5
+    eq_(u'<5>', lit % 5)
 
 def test_html():
     a = HTML.a(href='http://mostlysafe\" <tag', c="Bad <script> tag")
-    assert u'<a href="http://mostlysafe&quot; &lt;tag">Bad &lt;script&gt; tag</a>' == a
+    eq_(a, u'<a href="http://mostlysafe&#34; &lt;tag">Bad &lt;script&gt; tag</a>')
     
     img = HTML.img(src='http://some/image.jpg')
-    assert u'<img src="http://some/image.jpg" />' == img
+    eq_(img, u'<img src="http://some/image.jpg" />')
     
     br = HTML.br()
-    assert u'<br />' == br
+    eq_(u'<br />', br)
 
 def test_lit_re():
     lit = literal('This is a <string>')
     unlit = 'This is also a <string>'
     
     result = lit_sub(r'<str', literal('<b'), lit)
-    assert u'This is a <bing>' == escape(result)
+    eq_(u'This is a <bing>', escape(result))
     
     result = lit_sub(r'a <str', 'a <b> <b', unlit)
-    assert u'This is also a &lt;b&gt; &lt;bing&gt;' == escape(result)
+    eq_(u'This is also a &lt;b&gt; &lt;bing&gt;', escape(result))
 
 def test_unclosed_tag():
     result = HTML.form(_closed=False)
     print result
-    assert u'<form>' == result
+    eq_(u'<form>', result)
     
     result = HTML.form(_closed=False, action="hello")
-    assert u'<form action="hello">' == result
+    eq_(u'<form action="hello">', result)
 
 def test_newline_arg():
     eq_(HTML.a(),         literal(u'<a></a>'))
     eq_(HTML.a(_nl=True), literal(u'<a>\n</a>\n'))
     eq_(HTML.a(_closed=False),           literal(u'<a>'))
     eq_(HTML.a(_closed=False, _nl=True), literal(u'<a>\n'))
-    eq_(HTML.a("A", "B", href="/"),         literal(u'<a href="/">AB</a>'))
+    eq_(HTML.a("A", "B", href="/"),      literal(u'<a href="/">AB</a>'))
     eq_(HTML.a("A", "B", href="/", _nl=True), literal(u'<a href="/">\nA\nB\n</a>\n'))

tests/test_paginate.py

 """"Test webhelpers.paginate package."""
 import sys
 
+from nose.tools import eq_
 from routes import Mapper
 
 from webhelpers.paginate import Page
     """Test that 100 items fit on seven 15-item pages."""
     items = range(100)
     page = Page(items, page=0, items_per_page=15, url=my_url_generator)
-    assert page.page == 1
-    assert page.first_item == 1
-    assert page.last_item == 15
-    assert page.first_page == 1
-    assert page.last_page == 7
+    eq_(page.page, 1)
+    eq_(page.first_item, 1)
+    eq_(page.last_item, 15)
+    eq_(page.first_page, 1)
+    eq_(page.last_page, 7)
     assert page.previous_page is None
-    assert page.next_page == 2
-    assert page.items_per_page == 15
-    assert page.item_count == 100
-    assert page.page_count == 7
-    assert page.pager() == '<span class="pager_curpage">1</span> <a class="pager_link" href="/content?page=2">2</a> <a class="pager_link" href="/content?page=3">3</a> <span class="pager_dotdot">..</span> <a class="pager_link" href="/content?page=7">7</a>'
-    assert page.pager(separator='_') == '<span class="pager_curpage">1</span>_<a class="pager_link" href="/content?page=2">2</a>_<a class="pager_link" href="/content?page=3">3</a>_<span class="pager_dotdot">..</span>_<a class="pager_link" href="/content?page=7">7</a>'
-    assert page.pager(page_param='xy') == '<span class="pager_curpage">1</span> <a class="pager_link" href="/content?xy=2">2</a> <a class="pager_link" href="/content?xy=3">3</a> <span class="pager_dotdot">..</span> <a class="pager_link" href="/content?xy=7">7</a>'
-    assert page.pager(link_attr={'style':'s1'}, curpage_attr={'style':'s2'}, dotdot_attr={'style':'s3'}) == '<span style="s2">1</span> <a href="/content?page=2" style="s1">2</a> <a href="/content?page=3" style="s1">3</a> <span style="s3">..</span> <a href="/content?page=7" style="s1">7</a>'
-    assert page.pager(onclick="empty") == '<span class="pager_curpage">1</span> <a class="pager_link" href="/content?page=2" onclick="empty">2</a> <a class="pager_link" href="/content?page=3" onclick="empty">3</a> <span class="pager_dotdot">..</span> <a class="pager_link" href="/content?page=7" onclick="empty">7</a>'
-    assert page.pager(onclick="load('$page')") == '<span class="pager_curpage">1</span> <a class="pager_link" href="/content?page=2" onclick="load(\'2\')">2</a> <a class="pager_link" href="/content?page=3" onclick="load(\'3\')">3</a> <span class="pager_dotdot">..</span> <a class="pager_link" href="/content?page=7" onclick="load(\'7\')">7</a>'
+    eq_(page.next_page, 2)
+    eq_(page.items_per_page, 15)
+    eq_(page.item_count, 100)
+    eq_(page.page_count, 7)
+    eq_(page.pager(), '<span class="pager_curpage">1</span> <a class="pager_link" href="/content?page=2">2</a> <a class="pager_link" href="/content?page=3">3</a> <span class="pager_dotdot">..</span> <a class="pager_link" href="/content?page=7">7</a>')
+    eq_(page.pager(separator='_'), '<span class="pager_curpage">1</span>_<a class="pager_link" href="/content?page=2">2</a>_<a class="pager_link" href="/content?page=3">3</a>_<span class="pager_dotdot">..</span>_<a class="pager_link" href="/content?page=7">7</a>')
+    eq_(page.pager(page_param='xy'), '<span class="pager_curpage">1</span> <a class="pager_link" href="/content?xy=2">2</a> <a class="pager_link" href="/content?xy=3">3</a> <span class="pager_dotdot">..</span> <a class="pager_link" href="/content?xy=7">7</a>')
+    eq_(page.pager(link_attr={'style':'s1'}, curpage_attr={'style':'s2'}, dotdot_attr={'style':'s3'}), '<span style="s2">1</span> <a href="/content?page=2" style="s1">2</a> <a href="/content?page=3" style="s1">3</a> <span style="s3">..</span> <a href="/content?page=7" style="s1">7</a>')
+    eq_(page.pager(onclick="empty"), '<span class="pager_curpage">1</span> <a class="pager_link" href="/content?page=2" onclick="empty">2</a> <a class="pager_link" href="/content?page=3" onclick="empty">3</a> <span class="pager_dotdot">..</span> <a class="pager_link" href="/content?page=7" onclick="empty">7</a>')
+    eq_(page.pager(onclick="load('$page')"), '<span class="pager_curpage">1</span> <a class="pager_link" href="/content?page=2" onclick="load(&#39;2&#39;)">2</a> <a class="pager_link" href="/content?page=3" onclick="load(&#39;3&#39;)">3</a> <span class="pager_dotdot">..</span> <a class="pager_link" href="/content?page=7" onclick="load(&#39;7&#39;)">7</a>')
     if not sys.platform.startswith('java'):
         # XXX: these assume dict ordering
-        assert page.pager(onclick="load('%s')") == '<span class="pager_curpage">1</span> <a class="pager_link" href="/content?page=2" onclick="load(\'/content?partial=1&amp;page=2\')">2</a> <a class="pager_link" href="/content?page=3" onclick="load(\'/content?partial=1&amp;page=3\')">3</a> <span class="pager_dotdot">..</span> <a class="pager_link" href="/content?page=7" onclick="load(\'/content?partial=1&amp;page=7\')">7</a>'
-        assert page.pager(onclick="load('$partial_url')") == '<span class="pager_curpage">1</span> <a class="pager_link" href="/content?page=2" onclick="load(\'/content?partial=1&amp;page=2\')">2</a> <a class="pager_link" href="/content?page=3" onclick="load(\'/content?partial=1&amp;page=3\')">3</a> <span class="pager_dotdot">..</span> <a class="pager_link" href="/content?page=7" onclick="load(\'/content?partial=1&amp;page=7\')">7</a>'
+        eq_(page.pager(onclick="load('%s')"), '<span class="pager_curpage">1</span> <a class="pager_link" href="/content?page=2" onclick="load(&#39;/content?partial=1&amp;page=2&#39;)">2</a> <a class="pager_link" href="/content?page=3" onclick="load(&#39;/content?partial=1&amp;page=3&#39;)">3</a> <span class="pager_dotdot">..</span> <a class="pager_link" href="/content?page=7" onclick="load(&#39;/content?partial=1&amp;page=7&#39;)">7</a>')
+        eq_(page.pager(onclick="load('$partial_url')"), '<span class="pager_curpage">1</span> <a class="pager_link" href="/content?page=2" onclick="load(&#39;/content?partial=1&amp;page=2&#39;)">2</a> <a class="pager_link" href="/content?page=3" onclick="load(&#39;/content?partial=1&amp;page=3&#39;)">3</a> <span class="pager_dotdot">..</span> <a class="pager_link" href="/content?page=7" onclick="load(&#39;/content?partial=1&amp;page=7&#39;)">7</a>')

tests/test_tags.py

 
 from nose.tools import eq_
 
-from webhelpers.html import HTML
+from webhelpers.html import HTML, literal
 from webhelpers.html.tags import *
 
 from util import raises
                link_to(None, HTML.literal("http://www.example.com?q1=v1&amp;q2=v2")))
     
     def test_link_tag_with_custom_onclick(self):
-        eq_(u"<a href=\"http://www.example.com\" onclick=\"alert('yay!')\">Hello</a>", 
+        eq_(u"<a href=\"http://www.example.com\" onclick=\"alert(&#39;yay!&#39;)\">Hello</a>", 
                link_to("Hello", "http://www.example.com", onclick="alert('yay!')"))
     
 
 class TestAssetTagHelper(object):
     def test_auto_discovery_link_tag(self):
-        eq_('<link href="http://feed.com/feed.xml" rel="alternate" title="RSS" type="application/rss+xml" />',
+        eq_(literal(u'<link href="http://feed.com/feed.xml" rel="alternate" title="RSS" type="application/rss+xml" />'),
                          auto_discovery_link('http://feed.com/feed.xml'))
         eq_('<link href="http://feed.com/feed.xml" rel="alternate" title="ATOM" type="application/atom+xml" />',
                          auto_discovery_link('http://feed.com/feed.xml', feed_type='atom'))
                          javascript_link('/js/pngfix.js', defer=True))
 
     def test_stylesheet_link_tag(self):
-        eq_('<link href="/dir/file.css" media="all" rel="stylesheet" type="text/css" />',
+        eq_(literal(u'<link href="/dir/file.css" media="all" rel="stylesheet" type="text/css" />'),
                          stylesheet_link('/dir/file.css', media='all'))
         eq_('<link href="style.css" media="all" rel="stylesheet" type="text/css" />',
                          stylesheet_link('style.css', media='all'))

webhelpers/html/builder.py

 
 * a smart ``escape()`` function that preserves literals but
   escapes other strings that may accidentally contain markup characters ("<",
-  ">", "&") or malicious Javascript tags.  Escaped strings are returned as
-  literals to prevent them from being double-escaped later.
+  ">", "&", '"', "'") or malicious Javascript tags.  Escaped strings are
+  returned as literals to prevent them from being double-escaped later.
 
 ``literal`` is a subclass of ``unicode``, so it works with all string methods
 and expressions.  The only thing special about it is the ``.__html__`` method,
 an ``.__html__`` method in their own classes returning the desired HTML
 representation.
 
-When used in a mixed expression containing both literals and ordinary strings,
-``literal`` tries hard to escape the strings and return a literal.  However,
-this depends on which value has "control" of the expression.  ``literal`` seems
-to be able to take control with all combinations of the ``+`` operator, but
-with ``%`` and ``join`` it must be on the left side of the expression.  So
-these all work::
+WebHelpers 1.2 uses MarkupSafe, a package which provides an enhanced
+implementation of this protocol. Mako and Pylons have also switched to
+MarkupSafe. MarkupSafe advantages are a C speedup for escaping,
+escaping single-quotes for security, and adding new methods to
+``literal``. **literal** is now a subclass of ``markupsafe.Markup``.
+**escape** is ``markupsafe.escape_silent``. (The latter does not exist yet in
+MarkupSafe 0.9.3, but WebHelpers itself converts None to "" in the meantime). 
+
+Single-quote escaping affects HTML attributes that are written like this:
+*alt='Some text.'* rather than the normal *alt="Some text."*  If the text is a
+replaceable parameter whose value contains a single quote, the browser would
+think the value ends earlier than it does, thus enabling a potential cross-site
+scripting (XSS) attack. WebHelpers 1.0 and earlier escaped double quotes but
+not single quotes. MarkupSafe escapes both double and single quotes, preventing
+this sort of attack.
+
+MarkupSafe has some slight differences which should not cause compatibility
+issues but may in the following edge cases.  (A) The ``force`` argument to
+``escape()`` is gone. We doubt it was ever used. (B) The default encoding of
+``literal()`` is "ascii" instead of "utf-8". (C) Double quotes are escaped as
+"&#34;" instead of "&quot;". Single quotes are escaped as "&#39;". 
+
+When ``literal`` is used in a mixed expression containing both literals and
+ordinary strings, it tries hard to escape the strings and return a literal.
+However, this depends on which value has "control" of the expression.
+``literal`` seems to be able to take control with all combinations of the ``+``
+operator, but with ``%`` and ``join`` it must be on the left side of the
+expression.  So these all work::
 
     "A" + literal("B")
     literal(", ").join(["A", literal("B")])
 import re
 from urllib import quote as url_escape
 from UserDict import DictMixin
+
+import markupsafe
 try:
-    set
-except NameError:
-    from sets import Set as set
+    from markupsafe import escape_silent as escape
+except ImportError:
+    def escape(s):
+        if s is None:
+            return EMPTY
+        return markupsafe.escape(s)
 
-from webhelpers.util import cgi_escape
+class literal(markupsafe.Markup):
+    """Represents an HTML literal.
+    
+    This subclass of unicode has a ``.__html__()`` method that is 
+    detected by the ``escape()`` function.
+    
+    Also, if you add another string to this string, the other string 
+    will be quoted and you will get back another literal object.  Also
+    ``literal(...) % obj`` will quote any value(s) from ``obj``.  If
+    you do something like ``literal(...) + literal(...)``, neither
+    string will be changed because ``escape(literal(...))`` doesn't
+    change the original literal.
+
+    Changed in WebHelpers 1.2: the implementation is now now a subclass of
+    ``markupsafe.Markup``.  This brings some new methods: ``.escape`` (class
+    method), ``.unescape``, and ``.striptags``.
+    
+    """
+    __slots__ = ()
+
+    @classmethod
+    def escape(cls, s):
+        if s is None:
+            return EMPTY
+        return super(literal, cls).escape(s)
+
 
 __all__ = ["HTML", "escape", "literal", "url_escape", "lit_sub"]
 
         if value is not None]
     return literal("".join(strings))
 
-class literal(unicode):
-    """Represents an HTML literal.
-    
-    This subclass of unicode has a ``.__html__()`` method that is 
-    detected by the ``escape()`` function.
-    
-    Also, if you add another string to this string, the other string 
-    will be quoted and you will get back another literal object.  Also
-    ``literal(...) % obj`` will quote any value(s) from ``obj``.  If
-    you do something like ``literal(...) + literal(...)``, neither
-    string will be changed because ``escape(literal(...))`` doesn't
-    change the original literal.
-    
-    """
-    def __new__(cls, string='', encoding='utf-8', errors="strict"):
-        """Create the new literal string object."""
-        if isinstance(string, unicode):
-            obj = unicode.__new__(cls, string)
-        else:
-            obj = unicode.__new__(cls, string, encoding, errors)
-        obj.encoding = encoding
-        obj.error_mode = errors
-        return obj
-
-    def __str__(self):
-        return self.encode(self.encoding)
-
-    def __repr__(self):
-        return '%s(%s)' % (self.__class__.__name__, unicode.__repr__(self))
-        
-    def __html__(self):
-        return self
-        
-    def __add__(self, other):
-        if hasattr(other, '__html__') or isinstance(other, basestring):
-            return self.__class__(unicode.__add__(self, escape(other)))
-        return NotImplemented
-        
-    def __radd__(self, other):
-        if hasattr(other, '__html__') or isinstance(other, basestring):
-            return self.__class__(unicode.__add__(escape(other), self))
-        return NotImplemented
-    
-    def __mul__(self, count):
-        return self.__class__(unicode.__mul__(self, count))
-    
-    def __mod__(self, obj):
-        if isinstance(obj, tuple):
-            escaped = [_EscapedItem(item, self.encoding,
-                                    self.error_mode) for item in obj]
-            return self.__class__(unicode.__mod__(self, tuple(escaped)))
-        else:
-            return self.__class__(unicode.__mod__(self, _EscapedItem(obj, self.encoding,
-                                                                     self.error_mode)))
-        
-    def join(self, items):
-        return self.__class__(unicode.join(self, ([escape(i) for i in items])))
-    
-    def split(self, *args, **kwargs):
-        return [literal(x) for x in unicode.split(self, *args, **kwargs)]
-
-    def rsplit(self, *args, **kwargs):
-        return [literal(x) for x in unicode.rsplit(self, *args, **kwargs)]
-    
-    def splitlines(self, *args, **kwargs):
-        return [literal(x) for x in unicode.splitlines(self, *args, **kwargs)]
-
-
-# Yes, this is rather sucky, but I really don't want to write all these
-# damn methods, so we write in all the appropriate literal results of these
-# functions on module load
-for k in dir(literal):
-    if k in ['__getslice__', '__getitem__', 'capitalize', 'center', 
-             'expandtabs', 'ljust', 'lower', 'lstrip', 'partition',
-             'replace', 'rjust', 'rpartition', 'rstrip', 'strip',
-             'swapcase', 'title', 'translate', 'upper', 'zfill']:
-        def wrapper(func):
-            def entangle(*args, **kwargs):
-                return literal(func(*args, **kwargs))
-            try:
-                entangle.__name__ = func.__name__
-            except TypeError:
-                # < Python 2.4 
-                pass
-            entangle.__doc__ = func.__doc__
-            return entangle
-        fun = getattr(unicode, k)
-        setattr(literal, k, wrapper(fun))
-
 
 def lit_sub(*args, **kw):
     """Literal-safe version of re.sub.  If the string to be operated on is
         return result
 
 
-def escape(val, force=False):
-    """Does HTML-escaping of a value.
-    
-    Objects with a ``.__html__()`` method will have that method called,
-    and the return value will *not* be quoted.  Thus objects with that
-    magic method can be used to represent HTML that should not be
-    quoted.
-    
-    As a special case, ``escape(None)`` returns ''
-    
-    If ``force`` is true, then it will always be quoted regardless of
-    ``__html__()``.
-    
-    """
-    if val is None:
-        return literal('')
-    elif not force and hasattr(val, '__html__'):
-        return literal(val.__html__())
-    elif isinstance(val, basestring):
-        return literal(cgi_escape(val, True))
-    else:
-        return literal(cgi_escape(unicode(val), True))
-
-class _EscapedItem(DictMixin):
-    
-    """Wrapper/helper for literal(...) % obj
-    
-    This quotes the object during string substitution, and if the
-    object is dictionary(-like) it will quote all the values in the
-    dictionary.
-    
-    """
-    
-    def __init__(self, obj, encoding, error_mode):
-        self.obj = obj
-        self.encoding = encoding
-        self.error_mode = error_mode
-        
-    def __getitem__(self, key):
-        return _EscapedItem(self.obj[key], self.encoding, self.error_mode)
-        
-    def __str__(self):
-        v = escape(self.obj)
-        if isinstance(v, unicode):
-            v = v.encode(self.encoding)
-        return v
-        
-    def __unicode__(self):
-        v = escape(self.obj)
-        if isinstance(v, str):
-            v = v.decode(self.encoding, self.error_mode)
-        return v
-    
-    def __int__(self):
-        return int(self.obj)
-    
-    def __float__(self):
-        return float(self.obj)
-    
-    def __repr__(self):
-        return escape(repr(self.obj))
-
-
-empty_tags = set("area base basefont br col frame hr img input isindex link meta param".split())
+empty_tags = set(["area", "base", "basefont", "br", "col", "frame", "hr",
+    "img", "input", "isindex", "link", "meta", "param"])
 
 HTML = HTMLBuilder()
 
 # Constants depending on ``literal()`` and/or ``HTML``.
-NL = literal("\n")
-EMPTY = literal("")
+NL = literal(u"\n")
+EMPTY = literal(u"")
 BR = HTML.br(_nl=True)
 _CDATA_START = literal(u"<![CDATA[") 
 _CDATA_END = literal(u"]]>")

webhelpers/util.py

     should call ``webhelpers.html.builder.escape()`` instead of this to prevent
     double-escaping.
 
+    Changed in WebHelpers 1.2: escape single-quote as well as double-quote.
+
     """
     # Called by webhelpers.html.builder
     if '&' in s:
         s = s.replace(">", "&gt;")
     if quote:
         s = s.replace('"', "&quot;")
+        s = s.replace("'", "&apos;")
     return s
 
 def html_escape(s):
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.