[silva.core.editor][Emiliano D'Alterio] Changed the HTML Sanitiz...
[email protected] Mon, 26 Aug 2013 16:35:18 +0200
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élas! mon ami, l'époque est triste, et mes contes, je vous en préviens,
-
- 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">source</a>
- </p>
- <ul><li>French</li>
- <li>English</li>
- <li>Netherlands</li>
- </ul>
-</div>
-"""
+ <div>
+ <p data-timestamp="42">
+ Hélas! mon ami, l'époque est triste, et mes contes, je vous en préviens,
+
+ 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">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)
-