[silva.core.editor][Emiliano D'Alterio] Changed the HTML Sanitiz...

[email protected]
Newsgroups gmane.comp.web.zope.silva.cvs
Message-ID <[email protected]>
author:    Emiliano D'Alterio
date:      Mon Aug 26 16:33:50 2013 +0200
revision:  262:01ed90c1cc50 in silva.core.editor
branch:    
details:   https://hg.infrae.com/silva.core.editor?cmd=changeset;node=01ed90c1cc50
modified:  src/silva/core/editor/interfaces.py src/silva/core/editor/service.py src/silva/core/editor/tests/test_utils.py src/silva/core/editor/transform/editor/output.py src/silva/core/editor/utils.py
added:     
removed:   
log:       Changed the HTML Sanitizer for the CKEditor service to support per-
	tag basis allowed attributes lists. Added 3 tests for the HTML
	Sanitizer.


diffstat:

 src/silva/core/editor/interfaces.py              |   40 +++
 src/silva/core/editor/service.py                 |   42 +-
 src/silva/core/editor/tests/test_utils.py        |  291 +++++++++++++---------
 src/silva/core/editor/transform/editor/output.py |   39 +-
 src/silva/core/editor/utils.py                   |  279 ++++++++++-----------
 5 files changed, 398 insertions(+), 293 deletions(-)

diffs (911 lines):

diff -r a0c9fa0ee9cb -r 01ed90c1cc50 src/silva/core/editor/interfaces.py
--- a/src/silva/core/editor/interfaces.py	Mon Aug 19 16:55:21 2013 +0200
+++ b/src/silva/core/editor/interfaces.py	Mon Aug 26 16:33:50 2013 +0200
@@ -357,3 +357,43 @@
     def clear():
         """Clear all indexes.
         """
+
+
+class IPerTagAllowedAttributes(interface.Interface):
+    """An allowed HTML tag and its allowed attributes and CSS properties
+    """
+    html_tag = schema.TextLine(
+        title=u"Allowed HTML tag",
+        required=True)
+    html_attributes = schema.Set(
+        title=u"Allowed HTML attributes",
+        value_type=schema.TextLine(),
+        required=False)
+    css_properties = schema.Set(
+        title=u"Allowed CSS properties",
+        value_type=schema.TextLine(),
+        required=False)
+
+
+class PerTagAllowedAttributes(object):
+    grok.implements(IPerTagAllowedAttributes)
+
+    def __init__(self, html_tag, html_attributes=set(), css_properties=set()):
+        self.html_tag = html_tag
+        self.html_attributes = set(html_attributes)
+        self.css_properties = set(css_properties)
+
+    def __hash__(self):
+        return hash(self.html_tag)
+
+    def __eq__(self, other):
+        if not isinstance(other, PerTagAllowedAttributes):
+            return NotImplemented
+        return self.html_tag == other.html_tag
+
+
+grok.global_utility(
+    PerTagAllowedAttributes,
+    provides=IFactory,
+    name=IPerTagAllowedAttributes.__identifier__,
+    direct=True)
diff -r a0c9fa0ee9cb -r 01ed90c1cc50 src/silva/core/editor/service.py
--- a/src/silva/core/editor/service.py	Mon Aug 19 16:55:21 2013 +0200
+++ b/src/silva/core/editor/service.py	Mon Aug 26 16:33:50 2013 +0200
@@ -33,9 +33,11 @@
 
 from .interfaces import ICKEditorService
 from .interfaces import ICKEditorSettings
-from .utils import HTML_TAGS_WHITELIST
-from .utils import HTML_ATTRIBUTES_WHITELIST
-from .utils import CSS_ATTRIBUTES_WHITELIST
+from .interfaces import IPerTagAllowedAttributes
+
+from .utils import DEFAULT_PER_TAG_WHITELISTS
+from .utils import DEFAULT_HTML_ATTR_WHITELIST
+from .utils import DEFAULT_CSS_PROP_WHITELIST
 
 
 logger = logging.getLogger('silva.core.editor')
@@ -150,7 +152,7 @@
          'action': 'manage_html_sanitizer'},) + SilvaService.manage_options
 
     _config_declarations = None
-    _allowed_html_tags = None
+    _per_tag_allowed_attr = None
     _allowed_html_attributes = None
     _allowed_css_attributes = None
 
@@ -158,9 +160,9 @@
         Folder.__init__(self, *args, **kw)
         SilvaService.__init__(self, *args, **kw)
         self._config_declarations = {}
-        self._allowed_html_tags = set(HTML_TAGS_WHITELIST)
-        self._allowed_html_attributes = set(HTML_ATTRIBUTES_WHITELIST)
-        self._allowed_css_attributes = set(CSS_ATTRIBUTES_WHITELIST)
+        self._per_tag_allowed_attr = set(DEFAULT_PER_TAG_WHITELISTS)
+        self._allowed_html_attributes = set(DEFAULT_HTML_ATTR_WHITELIST)
+        self._allowed_css_attributes = set(DEFAULT_CSS_PROP_WHITELIST)
 
     def get_configuration(self, name):
         names = [name]
@@ -209,17 +211,14 @@
                     extra_plugins[name] = '/'.join((base, path))
         return extra_plugins
 
-    def set_allowed_html_tags(self, tags):
-        self._allowed_html_tags = set(tags)
-
     def set_allowed_html_attributes(self, attributes):
         self._allowed_html_attributes = set(attributes)
 
     def set_allowed_css_attributes(self, attributes):
         self._allowed_css_attributes = set(attributes)
 
-    def get_allowed_html_tags(self):
-        return self._allowed_html_tags
+    def set_per_tag_allowed_attr(self, per_tag_allowed_attr_set):
+        self._per_tag_allowed_attr = set(per_tag_allowed_attr_set)
 
     def get_allowed_html_attributes(self):
         return self._allowed_html_attributes
@@ -227,6 +226,9 @@
     def get_allowed_css_attributes(self):
         return self._allowed_css_attributes
 
+    def get_per_tag_allowed_attr(self):
+        return self._per_tag_allowed_attr
+
 
 InitializeClass(CKEditorService)
 
@@ -381,12 +383,16 @@
 
 
 class ISanitizerConfiguration(Interface):
-    _allowed_html_tags = schema.Set(title=u"Allowed HTML tags",
-                                    value_type=schema.TextLine())
-    _allowed_html_attributes = schema.Set(title=u"Allowed HTML attributes",
-                                          value_type=schema.TextLine())
-    _allowed_css_attributes = schema.Set(title=u"Allowed CSS attributes",
-                                         value_type=schema.TextLine())
+    _per_tag_allowed_attr = schema.Set(
+        title=(u"Allowed HTML tags and \
+            PER TAG allowed HTML attributes and CSS properties"),
+        value_type=schema.Object(schema=IPerTagAllowedAttributes))
+    _allowed_html_attributes = schema.Set(
+        title=u"Globally allowed HTML attributes",
+        value_type=schema.TextLine())
+    _allowed_css_attributes = schema.Set(
+        title=u"Globally allowed CSS properties",
+        value_type=schema.TextLine())
 
 
 class CKEditorServiceHTMLSanitizerConfiguration(silvaforms.ZMIForm):
diff -r a0c9fa0ee9cb -r 01ed90c1cc50 src/silva/core/editor/tests/test_utils.py
--- a/src/silva/core/editor/tests/test_utils.py	Mon Aug 19 16:55:21 2013 +0200
+++ b/src/silva/core/editor/tests/test_utils.py	Mon Aug 26 16:33:50 2013 +0200
@@ -7,19 +7,23 @@
 
 from ..utils import html_truncate_characters, html_truncate_words
 from ..utils import html_extract_text, html_sanitize
-from ..utils import HTML_TAGS_WHITELIST, HTML_ATTRIBUTES_WHITELIST
+from ..utils import DEFAULT_PER_TAG_WHITELISTS
+from ..utils import DEFAULT_HTML_ATTR_WHITELIST
+from ..interfaces import PerTagAllowedAttributes
 
 from Products.Silva.testing import tests
 
 
 ELLIPSIS = u"…"
 
+
 def html_truncate_test_characters(max_length, html_data, append=ELLIPSIS):
     # Helper for test purposes
     html_tree = lxml.html.fromstring(html_data)
     html_truncate_characters(html_tree, max_length, append=append)
     return lxml.html.tostring(html_tree)
 
+
 def html_truncate_test_words(max_length, html_data, append=ELLIPSIS):
     # Helper for test purposes
     html_tree = lxml.html.fromstring(html_data)
@@ -97,10 +101,10 @@
 
     def test_html_extract_text(self):
         chunk = """
-<p>This is some text and <img alt="an image appears" src="#" />
-and then there
-<a href="#" title="Link title">is a link</a> then it is over.</p>
-"""
+                <p>This is some text and <img alt="an image appears" src="#" />
+                and then there
+                <a href="#" title="Link title">is a link</a> then it is over.</p>
+                """
         tree = lxml.html.fromstring(chunk)
 
         self.assertItemsEqual(
@@ -110,147 +114,204 @@
 
 
 class TestSanitize(unittest.TestCase):
+    allowed_html_tags = set([PerTagAllowedAttributes('a'),
+                             PerTagAllowedAttributes('div')])
 
     HTML_CHUNCK = u"""
-<div>
-    <script>
-    function displayDate()
-    {
-    document.getElementById("demo").innerHTML=Date();
-    }
-    </script>
-    <style type="text/css">
-        p {
-            font-size: 1.1em;
-            color: dark;
-        }
-    </style>
-    <!-- this is a comment -->
-    <p AttriBute="self" data-timestamp="42">
-        Hélas! mon ami, l'époque est triste, et mes contes, je vous en préviens,
-        <video width="320" height="240" controls="controls">
-          <source src="movie.mp4" type="video/mp4" />
-          <source src="movie.ogg" type="video/ogg" />
-          Your browser does not support the video tag.
-        </video>
-        ne seront pas gais. Seulement, vous permettrez que, lassé de ce que je vois se passer tous les jours
-        dans le monde réel, j'aille chercher mes récits dans le monde imaginaire. Hélas! j'ai bien peur que tous
-        les esprits un peu élevés, un peu poétiques, un peu rêveurs, n'en soient à cette heure où en est le mien, c'est-à-dire
-        à la recherche de l'idéal, le seul, refuge que Dieu nous laisse contre la réalité.
-        <a href="http://www.gutenberg.org/files/15208/15208-h/15208-h.htm" custom-attrib="attrib">source</a>
-        <object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" width="550" height="400" id="movie_name" align="middle">
-        <param name="movie" value="movie_name.swf"/>
-        <!--[if !IE]>-->
-        <object type="application/x-shockwave-flash" data="movie_name.swf" width="550" height="400">
-            <param name="movie" value="movie_name.swf"/>
-        <!--<![endif]-->
-            <a href="http://www.adobe.com/go/getflash">
-                <img src="http://www.adobe.com/images/shared/download_buttons/get_flash_player.gif" alt="Get Adobe Flash player"/>
-            </a>
-        <!--[if !IE]>-->
-        </object>
-        <!--<![endif]-->
-        </object>
-    </p>
-    <ul>
-        <li>French</li>
-        <li>English</li>
-        <li>Netherlands</li>
-    </ul>
-</div>
-"""
+                    <div>
+                        <script>
+                        function displayDate()
+                        {
+                        document.getElementById("demo").innerHTML=Date();
+                        }
+                        </script>
+                        <style type="text/css">
+                            p {
+                                font-size: 1.1em;
+                                color: dark;
+                            }
+                        </style>
+                        <!-- this is a comment -->
+                        <p AttriBute="self" data-timestamp="42">
+                            Hélas! mon ami, l'époque est triste, et mes contes, je vous en préviens,
+                            <video width="320" height="240" controls="controls">
+                              <source src="movie.mp4" type="video/mp4" />
+                              <source src="movie.ogg" type="video/ogg" />
+                              Your browser does not support the video tag.
+                            </video>
+                            ne seront pas gais. Seulement, vous permettrez que, lassé de ce que je vois se passer tous les jours
+                            dans le monde réel, j'aille chercher mes récits dans le monde imaginaire. Hélas! j'ai bien peur que tous
+                            les esprits un peu élevés, un peu poétiques, un peu rêveurs, n'en soient à cette heure où en est le mien, c'est-à-dire
+                            à la recherche de l'idéal, le seul, refuge que Dieu nous laisse contre la réalité.
+                            <a href="http://www.gutenberg.org/files/15208/15208-h/15208-h.htm" custom-attrib="attrib">source</a>
+                            <object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" width="550" height="400" id="movie_name" align="middle">
+                            <param name="movie" value="movie_name.swf"/>
+                            <!--[if !IE]>-->
+                            <object type="application/x-shockwave-flash" data="movie_name.swf" width="550" height="400">
+                                <param name="movie" value="movie_name.swf"/>
+                            <!--<![endif]-->
+                                <a href="http://www.adobe.com/go/getflash">
+                                    <img src="http://www.adobe.com/images/shared/download_buttons/get_flash_player.gif" alt="Get Adobe Flash player"/>
+                                </a>
+                            <!--[if !IE]>-->
+                            </object>
+                            <!--<![endif]-->
+                            </object>
+                        </p>
+                        <ul>
+                            <li>French</li>
+                            <li>English</li>
+                            <li>Netherlands</li>
+                        </ul>
+                    </div>
+                    """
 
     def test_sanitize_chunk(self):
         sanitized = html_sanitize(
-            self.HTML_CHUNCK, HTML_TAGS_WHITELIST, HTML_ATTRIBUTES_WHITELIST)
+            self.HTML_CHUNCK, DEFAULT_PER_TAG_WHITELISTS, DEFAULT_HTML_ATTR_WHITELIST)
         expected = """
-<div>
-    <p data-timestamp="42">
-        H&#233;las! mon ami, l'&#233;poque est triste, et mes contes, je vous en pr&#233;viens,
-        
-        ne seront pas gais. Seulement, vous permettrez que, lass&#233; de ce que je vois se passer tous les jours
-        dans le monde r&#233;el, j'aille chercher mes r&#233;cits dans le monde imaginaire. H&#233;las! j'ai bien peur que tous
-        les esprits un peu &#233;lev&#233;s, un peu po&#233;tiques, un peu r&#234;veurs, n'en soient &#224; cette heure o&#249; en est le mien, c'est-&#224;-dire
-        &#224; la recherche de l'id&#233;al, le seul, refuge que Dieu nous laisse contre la r&#233;alit&#233;.
-        
-    <a href="http://www.gutenberg.org/files/15208/15208-h/15208-h.htm">source</a>
-        </p>
-    <ul><li>French</li>
-        <li>English</li>
-        <li>Netherlands</li>
-    </ul>
-</div>
-"""
+                    <div>
+                        <p data-timestamp="42">
+                            H&#233;las! mon ami, l'&#233;poque est triste, et mes contes, je vous en pr&#233;viens,
+                            
+                            ne seront pas gais. Seulement, vous permettrez que, lass&#233; de ce que je vois se passer tous les jours
+                            dans le monde r&#233;el, j'aille chercher mes r&#233;cits dans le monde imaginaire. H&#233;las! j'ai bien peur que tous
+                            les esprits un peu &#233;lev&#233;s, un peu po&#233;tiques, un peu r&#234;veurs, n'en soient &#224; cette heure o&#249; en est le mien, c'est-&#224;-dire
+                            &#224; la recherche de l'id&#233;al, le seul, refuge que Dieu nous laisse contre la r&#233;alit&#233;.
+
+                        <a href="http://www.gutenberg.org/files/15208/15208-h/15208-h.htm">source</a>
+                            </p>
+                        <ul><li>French</li>
+                            <li>English</li>
+                            <li>Netherlands</li>
+                        </ul>
+                    </div>
+                    """
         tests.assertXMLEqual(expected, sanitized)
 
     def test_sanitize_attributes(self):
         html = """
-<div>
-    <a href="http://infrae.com/" REL="media" CapitalizedNotAllowed="val">text</a>
-</div>
-"""
-        sanitized = html_sanitize(html, ['a', 'div'], ['href', 'rel'])
+                <div>
+                    <a href="http://infrae.com/" REL="media" CapitalizedNotAllowed="val">text</a>
+                </div>
+                """
+        sanitized = html_sanitize(html, self.allowed_html_tags, ['href', 'rel'])
         expected = """
-<div>
-    <a href="http://infrae.com/" rel="media">text</a>
-</div>
-"""
+                    <div>
+                        <a href="http://infrae.com/" rel="media">text</a>
+                    </div>
+                    """
         tests.assertXMLEqual(expected, sanitized)
 
     def test_sanitize_tags(self):
         html = """
-<div>
-    <A href="http://infrae.com/">text<FORM><input type="submit" value="button" /></FORM> after</A>
-</div>
-"""
-        sanitized = html_sanitize(html, ['a', 'div', 'input'], ['href'])
+                <div>
+                    <A href="http://infrae.com/">text<FORM><input type="submit" value="button" /></FORM> after</A>
+                </div>
+                """
+        allowed_html_tags = set([PerTagAllowedAttributes('a'),
+                                 PerTagAllowedAttributes('div'),
+                                 PerTagAllowedAttributes('input')])
+
+        sanitized = html_sanitize(html, allowed_html_tags, ['href'])
         expected = """
-<div>
-    <a href="http://infrae.com/">text after</a>
-</div>
-"""
+                    <div>
+                        <a href="http://infrae.com/">text after</a>
+                    </div>
+                    """
         tests.assertXMLEqual(expected, sanitized)
 
     def test_sanitize_css(self):
         html = """
-<div>
-    <a href="http://infrae.com/" style="text-decoration: underline; font-size: 20px; display:block">link</a>
-</div>
-"""
-        sanitized = html_sanitize(html, ['a', 'div'], ['href'], ['text-decoration', 'display'])
+                <div>
+                    <a href="http://infrae.com/" style="text-decoration: underline; font-size: 20px; display:block">link</a>
+                </div>
+                """
+        sanitized = html_sanitize(html, self.allowed_html_tags, ['href'], ['text-decoration', 'display'])
         expected = """
-<div>
-    <a href="http://infrae.com/" style="text-decoration: underline;display: block;">link</a>
-</div>
-"""
+                    <div>
+                        <a href="http://infrae.com/" style="text-decoration: underline;display: block;">link</a>
+                    </div>
+                    """
         tests.assertXMLEqual(expected, sanitized)
 
     def test_sanitize_css_with_error(self):
         html = """
-<div>
-    <a href="http://infrae.com/" style="text-decoration: underline; invalid\asdchunck">link</a>
-</div>
-"""
-        sanitized = html_sanitize(html, ['a', 'div'], ['href'], ['text-decoration'])
+                <div>
+                    <a href="http://infrae.com/" style="text-decoration: underline; invalid\asdchunck">link</a>
+                </div>
+                """
+        sanitized = html_sanitize(html, self.allowed_html_tags, ['href'], ['text-decoration'])
         expected = """
-<div>
-    <a href="http://infrae.com/" style="text-decoration: underline;">link</a>
-</div>
-"""
+                    <div>
+                        <a href="http://infrae.com/" style="text-decoration: underline;">link</a>
+                    </div>
+                    """
         tests.assertXMLEqual(expected, sanitized)
 
     def test_sanitize_css_with_error_first(self):
         html = """
-<div>
-    <a href="http://infrae.com/" style="invalid\asdchunck;text-decoration: underline;">link</a>
-</div>
-"""
-        sanitized = html_sanitize(html, ['a', 'div'], ['href'], ['text-decoration'])
+                <div>
+                    <a href="http://infrae.com/" style="invalid\asdchunck;text-decoration: underline;">link</a>
+                </div>
+                """
+        sanitized = html_sanitize(html, self.allowed_html_tags, ['href'], ['text-decoration'])
         expected = """
-<div>
-    <a href="http://infrae.com/" style="text-decoration: underline;">link</a>
-</div>
-"""
+                    <div>
+                        <a href="http://infrae.com/" style="text-decoration: underline;">link</a>
+                    </div>
+                    """
+        tests.assertXMLEqual(expected, sanitized)
+        
+    def test_sanitize_per_tag_css_properties(self):
+        html = """
+                <div style="color: red; background-color: blue; text-decoration: underline;">
+                    <a href="http://infrae.com/" style="color: red; background-color: blue; text-decoration: underline;">link</a>
+                </div>
+                """
+        allowed_html_tags = set([PerTagAllowedAttributes('a', css_properties=set(['color'])),
+                                 PerTagAllowedAttributes('div', css_properties=set(['background-color']))])
+
+        sanitized = html_sanitize(html, allowed_html_tags, ['href'], ['text-decoration'])
+        expected = """
+                    <div style="background-color: blue;text-decoration: underline;">
+                        <a href="http://infrae.com/" style="color: red;text-decoration: underline;">link</a>
+                    </div>
+                    """
+        tests.assertXMLEqual(expected, sanitized)
+
+    def test_sanitize_per_tag_html_attributes(self):
+        html = """
+                <div id="mydiv" class="myclass" title="mytitle" dir="rtl">
+                    <span id="mydiv" class="myclass" title="mytitle" dir="rtl">this is inside the span</span>
+                </div>
+                """
+        allowed_html_tags = set([PerTagAllowedAttributes('div', set(['class', 'id'])),
+                                 PerTagAllowedAttributes('span', set(['title']))])
+
+        sanitized = html_sanitize(html, allowed_html_tags, ['dir'])
+        expected = """
+                    <div id="mydiv" class="myclass" dir="rtl">
+                        <span title="mytitle" dir="rtl">this is inside the span</span>
+                    </div>
+                    """
+        tests.assertXMLEqual(expected, sanitized)
+
+    def test_sanitize_no_attributes_no_properties_allowed(self):
+        html = """
+                <div id="mydiv" class="myclass" title="mytitle" dir="rtl">
+                    <span id="mydiv" class="myclass" title="mytitle" dir="rtl">this is inside the span</span>
+                    <p>This is inside the p</p>
+                </div>
+                """
+        allowed_html_tags = set([PerTagAllowedAttributes('div'),
+                                 PerTagAllowedAttributes('span')])
+        sanitized = html_sanitize(html, allowed_html_tags, [])
+        expected = """
+                    <div>
+                        <span>this is inside the span</span>
+                    </div>
+                    """
         tests.assertXMLEqual(expected, sanitized)
 
 
diff -r a0c9fa0ee9cb -r 01ed90c1cc50 src/silva/core/editor/transform/editor/output.py
--- a/src/silva/core/editor/transform/editor/output.py	Mon Aug 19 16:55:21 2013 +0200
+++ b/src/silva/core/editor/transform/editor/output.py	Mon Aug 26 16:33:50 2013 +0200
@@ -14,10 +14,11 @@
 from silva.core.editor.transform.base import ReferenceTransformationFilter
 from silva.core.editor.transform.base import TransformationFilter
 from silva.core.editor.utils import html_sanitize_node
-from silva.core.editor.utils import HTML_TAGS_WHITELIST
-from silva.core.editor.utils import URL_SCHEMES_WHITELIST, URL_SCHEMES_BLACKLIST
-from silva.core.editor.utils import HTML_ATTRIBUTES_WHITELIST
-from silva.core.editor.utils import CSS_ATTRIBUTES_WHITELIST
+from silva.core.editor.utils import URL_SCHEMES_WHITELIST
+from silva.core.editor.utils import URL_SCHEMES_BLACKLIST
+from silva.core.editor.utils import DEFAULT_PER_TAG_WHITELISTS
+from silva.core.editor.utils import DEFAULT_HTML_ATTR_WHITELIST
+from silva.core.editor.utils import DEFAULT_CSS_PROP_WHITELIST
 
 
 def extract_url(url, url_schemes=URL_SCHEMES_WHITELIST):
@@ -61,7 +62,8 @@
     grok.provides(ISaveEditorFilter)
 
     def update_reference_for(self, attributes):
-        name, reference = self.get_reference(attributes['data-silva-reference'])
+        name, reference = self.get_reference(
+            attributes['data-silva-reference'])
         if reference is not None:
             target_id = attributes.get('data-silva-target', '0')
             try:
@@ -198,28 +200,33 @@
     grok.provides(ISaveEditorFilter)
     grok.order(1000)
 
-    _html_tags = None
+    _per_tag_allowed_attr = None
     _html_attributes = None
-    _extra_html_attributes = set(['reference', 'anchor', 'query', 'resolution'])
+    _extra_html_attributes = set(
+        ['reference', 'anchor', 'query', 'resolution'])
     _css_attributes = None
 
     def prepare(self, name, text):
         service = queryUtility(ICKEditorService)
+
         if service is not None:
-            self._html_tags = service.get_allowed_html_tags()
+            self._per_tag_allowed_attr = service.get_per_tag_allowed_attr()
             self._html_attributes = service.get_allowed_html_attributes()
             self._css_attributes = service.get_allowed_css_attributes()
-        if self._html_tags is None:
-            self._html_tags = HTML_TAGS_WHITELIST
+
+        if self._per_tag_allowed_attr is None:
+            self._per_tag_allowed_attr = DEFAULT_PER_TAG_WHITELISTS
         if self._html_attributes is None:
-            self._html_attributes = HTML_ATTRIBUTES_WHITELIST
+            self._html_attributes = DEFAULT_HTML_ATTR_WHITELIST
         if self._css_attributes is None:
-            self._css_attributes = CSS_ATTRIBUTES_WHITELIST
+            self._css_attributes = DEFAULT_CSS_PROP_WHITELIST
+
         self._html_attributes |= self._extra_html_attributes
 
     def __call__(self, tree):
-        if self._html_tags is not None and self._html_attributes is not None:
+        if (self._per_tag_allowed_attr is not None
+                and self._html_attributes is not None):
             html_sanitize_node(tree,
-                self._html_tags,
-                self._html_attributes,
-                self._css_attributes)
+                               self._per_tag_allowed_attr,
+                               self._html_attributes,
+                               self._css_attributes)
diff -r a0c9fa0ee9cb -r 01ed90c1cc50 src/silva/core/editor/utils.py
--- a/src/silva/core/editor/utils.py	Mon Aug 19 16:55:21 2013 +0200
+++ b/src/silva/core/editor/utils.py	Mon Aug 26 16:33:50 2013 +0200
@@ -6,9 +6,11 @@
 import lxml.html
 import re
 from tinycss import CSS21Parser
+from .interfaces import PerTagAllowedAttributes
 
 norm_whitespace_re = re.compile(r'[ \t\n]{2,}')
 
+
 def normalize_space(text, strip=False):
     if text is not None:
         if strip:
@@ -102,12 +104,13 @@
 WORD_PATTERN = re.compile(r'\s*[^\s]+\s*')
 RE_TRAIL_SPC = re.compile(r'\s*$')
 
+
 def html_truncate_words(el, remaining_words, append=u"…"):
     """Truncate the content of the lxml node ``el`` to contain no
     more than ``remaining_words`` words. ``append`` is appended to
     the end of the final truncated node, if any.
     """
-    found_words  = re.findall(WORD_PATTERN, el.text or u'')
+    found_words = re.findall(WORD_PATTERN, el.text or u'')
 
     if len(found_words) >= remaining_words:
         el.text = ''.join(found_words[:remaining_words])
@@ -116,7 +119,7 @@
         el.text += append
         el.tail = None
         for child in el.iterchildren():
-            el.remove(child);
+            el.remove(child)
         return 0
 
     remaining_words -= len(found_words)
@@ -132,7 +135,7 @@
     if not remaining_words:
         return 0
 
-    found_words  = re.findall(WORD_PATTERN, el.tail or u'')
+    found_words = re.findall(WORD_PATTERN, el.tail or u'')
 
     if len(found_words) >= remaining_words:
         el.tail = ''.join(found_words[:remaining_words])
@@ -150,161 +153,149 @@
 _DATA_ATTRIBUTE = 'data-'
 
 
-def html_sanitize_node(el, allowed_tags_set, allowed_attributes_set,
-        allowed_css_style_attributes_set=None):
-    attribute_names = set(el.attrib.iterkeys())
+def html_sanitize_node(el,
+                       per_tag_allowed_attr,
+                       allowed_attributes_set,
+                       allowed_css_style_attributes_set=None):
 
-    # CSS sanitizing
-    if allowed_css_style_attributes_set is not None and \
-        STYLE_ATTRIBUTE in attribute_names:
-        style = el.attrib[STYLE_ATTRIBUTE]
-        attribute_names.remove(STYLE_ATTRIBUTE)
-        rules, errors = CSS21Parser().parse_style_attr(style)
-        if not rules and errors:
-            del el.attrib[STYLE_ATTRIBUTE]
-        else:
-            style_buffer = bytearray()
-            for rule in rules:
-                if rule.name in allowed_css_style_attributes_set:
-                    style_buffer += _CSS_RULE_FORMAT % (
-                        rule.name.encode(UTF8),
-                        rule.value.as_css().encode(UTF8))
-            el.attrib[STYLE_ATTRIBUTE] = str(style_buffer)
+    allowed_tags = {}
+    for allowed_tag in per_tag_allowed_attr:
+        allowed_tags[allowed_tag.html_tag] = (
+            set(allowed_tag.html_attributes),
+            set(allowed_tag.css_properties))
 
-    # HTML attributes sanitizing
-    for attribute_name in attribute_names - allowed_attributes_set:
-        # We authorize data- attributes.
-        if not attribute_name.startswith(_DATA_ATTRIBUTE):
-            del el.attrib[attribute_name]
+    def recursive_html_sanitize_node(el):
+        attribute_names = set(el.attrib.iterkeys())
 
-    # HTML tags sanitizing
-    for child in el.iterchildren():
-        if not isinstance(child, lxml.html.HtmlElement):
-            el.remove(child)
-            continue
-        if child.tag in allowed_tags_set:
-            html_sanitize_node(child, allowed_tags_set, allowed_attributes_set,
-                allowed_css_style_attributes_set)
-        else:
-            el.remove(child)
-            if child.tail:
-                if el.text is not None:
-                    el.text += child.tail
-                else:
-                    el.text = child.tail
+        extra_allowed_html_attr_for_el = set()
+        extra_allowed_css_proper_for_el = set()
+        if el.tag in allowed_tags:
+            extra_allowed_html_attr_for_el = allowed_tags[el.tag][0]
+            extra_allowed_css_proper_for_el = allowed_tags[el.tag][1]
+
+        # CSS sanitizing
+        if ((allowed_css_style_attributes_set is not None
+             or extra_allowed_css_proper_for_el)
+                and STYLE_ATTRIBUTE in attribute_names):
+            style = el.attrib[STYLE_ATTRIBUTE]
+            attribute_names.remove(STYLE_ATTRIBUTE)
+            rules, errors = CSS21Parser().parse_style_attr(style)
+            if not rules and errors:
+                del el.attrib[STYLE_ATTRIBUTE]
+            else:
+                style_buffer = bytearray()
+                for rule in rules:
+                    if (rule.name in allowed_css_style_attributes_set
+                            or rule.name in extra_allowed_css_proper_for_el):
+                        style_buffer += _CSS_RULE_FORMAT % (
+                            rule.name.encode(UTF8),
+                            rule.value.as_css().encode(UTF8))
+                el.attrib[STYLE_ATTRIBUTE] = str(style_buffer)
+
+        # HTML attributes sanitizing
+        for attribute_name in (
+                attribute_names - (allowed_attributes_set |
+                                   extra_allowed_html_attr_for_el)):
+            # We authorize data- attributes.
+            if not attribute_name.startswith(_DATA_ATTRIBUTE):
+                del el.attrib[attribute_name]
+
+        # HTML tags sanitizing
+        for child in el.iterchildren():
+            if not isinstance(child, lxml.html.HtmlElement):
+                el.remove(child)
+                continue
+            if (child.tag in allowed_tags):
+                recursive_html_sanitize_node(child)
+            else:
+                el.remove(child)
+                if child.tail:
+                    if el.text is not None:
+                        el.text += child.tail
+                    else:
+                        el.text = child.tail
+
+    recursive_html_sanitize_node(el)
+
 
 URL_SCHEMES_BLACKLIST = set([
-        'javascript'])
+    'javascript'])
 
 URL_SCHEMES_WHITELIST = set([
-        'http', 'https', 'ftp', 'ftps', 'ssh', 'news', 'mailto',
-        'tel', 'webcal', 'itms', 'broken',
-        ])
+    'http', 'https', 'ftp', 'ftps', 'ssh', 'news', 'mailto',
+    'tel', 'webcal', 'itms', 'broken',
+    ])
 
-HTML_TAGS_WHITELIST = set([
-    "a",
-    "abbr",
-    "acronym",
-    "address",
-    "area",
-    "article",
-    "aside",
-    "blockquote",
-    "br",
-    "caption",
-    "col",
-    "colgroup",
-    "comment",
-    "dd",
-    "del",
-    "details",
-    "div",
-    "dl",
-    "dt",
-    "b",
-    "i",
-    "h1",
-    "h2",
-    "h3",
-    "h4",
-    "h5",
-    "h6",
-    "img",
-    "ins",
-    "label",
-    "legend",
-    "li",
-    "map",
-    "mark",
-    "nobr",
-    "ol",
-    "p",
-    "cite",
-    "code",
-    "em",
-    "strong",
-    "strike",
-    "pre",
-    "section",
-    "spacer",
-    "span",
-    "sub",
-    "sup",
-    "table",
-    "tbody",
-    "td",
-    "tfoot",
-    "th",
-    "thead",
-    "time",
-    "tr",
-    "ul",
-    "wbr",
+DEFAULT_PER_TAG_WHITELISTS = set([
+    PerTagAllowedAttributes('a', set(['name', 'target', 'href'])),
+    PerTagAllowedAttributes('br'),
+    PerTagAllowedAttributes('abbr'),
+    PerTagAllowedAttributes('acronym'),
+    PerTagAllowedAttributes('blockquote'),
+    PerTagAllowedAttributes('caption'),
+    PerTagAllowedAttributes('div'),
+    PerTagAllowedAttributes('h1'),
+    PerTagAllowedAttributes('h2'),
+    PerTagAllowedAttributes('h3'),
+    PerTagAllowedAttributes('h4'),
+    PerTagAllowedAttributes('h5'),
+    PerTagAllowedAttributes('h6'),
+    PerTagAllowedAttributes('dl'),
+    PerTagAllowedAttributes('dt'),
+    PerTagAllowedAttributes('dd'),
+    PerTagAllowedAttributes('pre'),
+    PerTagAllowedAttributes('img', set(['alt', 'src'])),
+    PerTagAllowedAttributes('li'),
+    PerTagAllowedAttributes('ol', set(['start', 'type']),
+                            set(['list-style-type'])),
+    PerTagAllowedAttributes('p'),
+    PerTagAllowedAttributes('em'),
+    PerTagAllowedAttributes('strong'),
+    PerTagAllowedAttributes('i'),
+    PerTagAllowedAttributes('b'),
+    PerTagAllowedAttributes('strike'),
+    PerTagAllowedAttributes('span'),
+    PerTagAllowedAttributes('sub'),
+    PerTagAllowedAttributes('sup'),
+    PerTagAllowedAttributes('table', set(['summary', 'dir', 'cols']),
+                            set(['width', 'height'])),
+    PerTagAllowedAttributes('tbody'),
+    PerTagAllowedAttributes('td', set(['colspan', 'rowspan', 'scope']),
+                            set(['text-align', 'vertical-align',
+                                 'white-space', 'width', 'height'])),
+    PerTagAllowedAttributes('th', set(['colspan', 'rowspan', 'scope']),
+                            set(['text-align', 'vertical-align',
+                                 'white-space', 'width', 'height'])),
+    PerTagAllowedAttributes('thead'),
+    PerTagAllowedAttributes('tr'),
+    PerTagAllowedAttributes('ul', set(['type']), set(['list-style-type']))
+    ])
+
+
+DEFAULT_HTML_ATTR_WHITELIST = set([
+    "id",
+    "class",
+    "title"
 ])
 
-HTML_ATTRIBUTES_WHITELIST = set([
-    "accesskey",
-    "alt",
-    "cite",
-    "class",
-    "colspan",
-    "coords",
-    "crossorigin",
-    "datetime",
-    "for",
-    "href",
-    "hreflang",
-    "id",
-    "ismap",
-    "media",
-    "min",
-    "name",
-    "rowspan",
-    "src",
-    "tabindex",
-    "target",
-    "title",
-    "translate",
-    "type",
-    "usemap",
-    "value",
+DEFAULT_CSS_PROP_WHITELIST = set([
+    'margin',
+    'margin-left',
+    'margin-right'
 ])
 
-CSS_ATTRIBUTES_WHITELIST = set([
-    'clear',
-    'list-style-type',
-    'margin',
-    'margin-left',
-])
 
 ####### All the code below is only used in tests ####################
 
-def html_sanitize(html_data, allowed_tags, allowed_attributes,
-                  allowed_css_style_attributes=None):
+def html_sanitize(html_data, per_tag_allowed_attr,
+                  global_allowed_html_attr, global_allowed_css_prop=None):
     html_tree = lxml.html.fromstring(html_data)
-    if allowed_css_style_attributes is not None:
-        allowed_css_style_attributes = set(allowed_css_style_attributes)
-    html_sanitize_node(html_tree, set(allowed_tags), set(allowed_attributes),
-        allowed_css_style_attributes)
+    if global_allowed_css_prop is not None:
+        global_allowed_css_prop = set(global_allowed_css_prop)
+
+    html_sanitize_node(html_tree,
+                       set(per_tag_allowed_attr),
+                       set(global_allowed_html_attr),
+                       global_allowed_css_prop)
+
     return lxml.html.tostring(html_tree)
-
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.