cmlenz@1: # -*- coding: utf-8 -*- cmlenz@1: # cmlenz@854: # Copyright (C) 2006-2009 Edgewall Software cmlenz@1: # All rights reserved. cmlenz@1: # cmlenz@1: # This software is licensed as described in the file COPYING, which cmlenz@1: # you should have received as part of this distribution. The terms cmlenz@230: # are also available at http://genshi.edgewall.org/wiki/License. cmlenz@1: # cmlenz@1: # This software consists of voluntary contributions made by many cmlenz@1: # individuals. For the exact contribution history, see the revision cmlenz@230: # history and logs, available at http://genshi.edgewall.org/log/. cmlenz@1: cmlenz@1: """Implementation of a number of stream filters.""" cmlenz@1: cmlenz@856: try: cmlenz@856: any cmlenz@856: except NameError: cmlenz@856: from genshi.util import any cmlenz@1: import re cmlenz@1: cmlenz@403: from genshi.core import Attrs, QName, stripentities cmlenz@571: from genshi.core import END, START, TEXT, COMMENT cmlenz@1: cmlenz@363: __all__ = ['HTMLFormFiller', 'HTMLSanitizer'] cmlenz@425: __docformat__ = 'restructuredtext en' cmlenz@275: cmlenz@275: cmlenz@275: class HTMLFormFiller(object): cmlenz@275: """A stream filter that can populate HTML forms from a dictionary of values. cmlenz@275: cmlenz@275: >>> from genshi.input import HTML cmlenz@275: >>> html = HTML('''
cmlenz@275: ...

cmlenz@275: ...
''') cmlenz@275: >>> filler = HTMLFormFiller(data={'foo': 'bar'}) cmlenz@853: >>> print(html | filler) cmlenz@275:
cmlenz@275:

cmlenz@275:
cmlenz@275: """ cmlenz@275: # TODO: only select the first radio button, and the first select option cmlenz@275: # (if not in a multiple-select) cmlenz@275: # TODO: only apply to elements in the XHTML namespace (or no namespace)? cmlenz@275: cmlenz@841: def __init__(self, name=None, id=None, data=None, passwords=False): cmlenz@275: """Create the filter. cmlenz@275: cmlenz@425: :param name: The name of the form that should be populated. If this cmlenz@425: parameter is given, only forms where the ``name`` attribute cmlenz@425: value matches the parameter are processed. cmlenz@425: :param id: The ID of the form that should be populated. If this cmlenz@425: parameter is given, only forms where the ``id`` attribute cmlenz@425: value matches the parameter are processed. cmlenz@425: :param data: The dictionary of form values, where the keys are the names cmlenz@425: of the form fields, and the values are the values to fill cmlenz@425: in. cmlenz@841: :param passwords: Whether password input fields should be populated. cmlenz@841: This is off by default for security reasons (for cmlenz@841: example, a password may end up in the browser cache) cmlenz@841: :note: Changed in 0.5.2: added the `passwords` option cmlenz@275: """ cmlenz@275: self.name = name cmlenz@275: self.id = id cmlenz@275: if data is None: cmlenz@275: data = {} cmlenz@275: self.data = data cmlenz@841: self.passwords = passwords cmlenz@275: cmlenz@439: def __call__(self, stream): cmlenz@277: """Apply the filter to the given stream. cmlenz@277: cmlenz@425: :param stream: the markup event stream to filter cmlenz@277: """ cmlenz@275: in_form = in_select = in_option = in_textarea = False cmlenz@275: select_value = option_value = textarea_value = None jonas@584: option_start = None jonas@584: option_text = [] jonas@584: no_option_value = False cmlenz@275: cmlenz@275: for kind, data, pos in stream: cmlenz@275: cmlenz@275: if kind is START: cmlenz@345: tag, attrs = data cmlenz@275: tagname = tag.localname cmlenz@275: cmlenz@275: if tagname == 'form' and ( cmlenz@345: self.name and attrs.get('name') == self.name or cmlenz@345: self.id and attrs.get('id') == self.id or cmlenz@275: not (self.id or self.name)): cmlenz@275: in_form = True cmlenz@275: cmlenz@275: elif in_form: cmlenz@275: if tagname == 'input': jruigrok@844: type = attrs.get('type', '').lower() cmlenz@275: if type in ('checkbox', 'radio'): cmlenz@345: name = attrs.get('name') cmlenz@471: if name and name in self.data: cmlenz@471: value = self.data[name] cmlenz@345: declval = attrs.get('value') cmlenz@275: checked = False cmlenz@275: if isinstance(value, (list, tuple)): cmlenz@275: if declval: jonas@584: checked = declval in [unicode(v) for v cmlenz@415: in value] cmlenz@275: else: cmlenz@856: checked = any(value) cmlenz@275: else: cmlenz@275: if declval: jonas@584: checked = declval == unicode(value) cmlenz@275: elif type == 'checkbox': cmlenz@275: checked = bool(value) cmlenz@275: if checked: cmlenz@403: attrs |= [(QName('checked'), 'checked')] cmlenz@345: elif 'checked' in attrs: cmlenz@345: attrs -= 'checked' jruigrok@844: elif type in ('', 'hidden', 'text') \ cmlenz@841: or type == 'password' and self.passwords: cmlenz@345: name = attrs.get('name') cmlenz@471: if name and name in self.data: cmlenz@471: value = self.data[name] cmlenz@275: if isinstance(value, (list, tuple)): cmlenz@275: value = value[0] cmlenz@275: if value is not None: cmlenz@841: attrs |= [ cmlenz@841: (QName('value'), unicode(value)) cmlenz@841: ] cmlenz@275: elif tagname == 'select': cmlenz@345: name = attrs.get('name') cmlenz@471: if name in self.data: cmlenz@471: select_value = self.data[name] cmlenz@471: in_select = True cmlenz@275: elif tagname == 'textarea': cmlenz@345: name = attrs.get('name') cmlenz@471: if name in self.data: cmlenz@471: textarea_value = self.data.get(name) cmlenz@471: if isinstance(textarea_value, (list, tuple)): cmlenz@471: textarea_value = textarea_value[0] cmlenz@471: in_textarea = True cmlenz@275: elif in_select and tagname == 'option': cmlenz@275: option_start = kind, data, pos cmlenz@345: option_value = attrs.get('value') jonas@584: if option_value is None: jonas@584: no_option_value = True jonas@584: option_value = '' cmlenz@275: in_option = True cmlenz@275: continue cmlenz@345: yield kind, (tag, attrs), pos cmlenz@345: cmlenz@275: elif in_form and kind is TEXT: cmlenz@275: if in_select and in_option: jonas@584: if no_option_value: jonas@584: option_value += data jonas@584: option_text.append((kind, data, pos)) cmlenz@275: continue cmlenz@275: elif in_textarea: cmlenz@275: continue cmlenz@345: yield kind, data, pos cmlenz@275: cmlenz@275: elif in_form and kind is END: cmlenz@275: tagname = data.localname cmlenz@275: if tagname == 'form': cmlenz@275: in_form = False cmlenz@275: elif tagname == 'select': cmlenz@275: in_select = False cmlenz@275: select_value = None cmlenz@275: elif in_select and tagname == 'option': cmlenz@275: if isinstance(select_value, (tuple, list)): jonas@584: selected = option_value in [unicode(v) for v cmlenz@415: in select_value] cmlenz@275: else: jonas@584: selected = option_value == unicode(select_value) cmlenz@345: okind, (tag, attrs), opos = option_start cmlenz@275: if selected: cmlenz@403: attrs |= [(QName('selected'), 'selected')] cmlenz@345: elif 'selected' in attrs: cmlenz@345: attrs -= 'selected' cmlenz@345: yield okind, (tag, attrs), opos cmlenz@275: if option_text: jonas@584: for event in option_text: jonas@584: yield event cmlenz@275: in_option = False jonas@584: no_option_value = False jonas@584: option_start = option_value = None jonas@584: option_text = [] cmlenz@275: elif tagname == 'textarea': cmlenz@275: if textarea_value: cmlenz@275: yield TEXT, unicode(textarea_value), pos cmlenz@275: in_textarea = False cmlenz@345: yield kind, data, pos cmlenz@275: cmlenz@345: else: cmlenz@345: yield kind, data, pos cmlenz@123: cmlenz@123: cmlenz@123: class HTMLSanitizer(object): cmlenz@123: """A filter that removes potentially dangerous HTML tags and attributes cmlenz@123: from the stream. cmlenz@431: cmlenz@431: >>> from genshi import HTML cmlenz@431: >>> html = HTML('
') cmlenz@853: >>> print(html | HTMLSanitizer()) cmlenz@431:
cmlenz@431: cmlenz@431: The default set of safe tags and attributes can be modified when the filter cmlenz@431: is instantiated. For example, to allow inline ``style`` attributes, the cmlenz@431: following instantation would work: cmlenz@431: cmlenz@431: >>> html = HTML('
') cmlenz@431: >>> sanitizer = HTMLSanitizer(safe_attrs=HTMLSanitizer.SAFE_ATTRS | set(['style'])) cmlenz@853: >>> print(html | sanitizer) cmlenz@431:
cmlenz@431: cmlenz@431: Note that even in this case, the filter *does* attempt to remove dangerous cmlenz@431: constructs from style attributes: cmlenz@431: cmlenz@431: >>> html = HTML('
') cmlenz@853: >>> print(html | sanitizer) cmlenz@431:
cmlenz@431: cmlenz@431: This handles HTML entities, unicode escapes in CSS and Javascript text, as cmlenz@431: well as a lot of other things. However, the style tag is still excluded by cmlenz@431: default because it is very hard for such sanitizing to be completely safe, cmlenz@431: especially considering how much error recovery current web browsers perform. cmlenz@571: cmlenz@840: It also does some basic filtering of CSS properties that may be used for cmlenz@840: typical phishing attacks. For more sophisticated filtering, this class cmlenz@840: provides a couple of hooks that can be overridden in sub-classes. cmlenz@840: cmlenz@571: :warn: Note that this special processing of CSS is currently only applied to cmlenz@571: style attributes, **not** style elements. cmlenz@123: """ cmlenz@123: cmlenz@277: SAFE_TAGS = frozenset(['a', 'abbr', 'acronym', 'address', 'area', 'b', cmlenz@123: 'big', 'blockquote', 'br', 'button', 'caption', 'center', 'cite', cmlenz@123: 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl', 'dt', cmlenz@123: 'em', 'fieldset', 'font', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', cmlenz@123: 'hr', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'map', cmlenz@123: 'menu', 'ol', 'optgroup', 'option', 'p', 'pre', 'q', 's', 'samp', cmlenz@123: 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'table', cmlenz@123: 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'tr', 'tt', 'u', cmlenz@123: 'ul', 'var']) cmlenz@123: cmlenz@277: SAFE_ATTRS = frozenset(['abbr', 'accept', 'accept-charset', 'accesskey', cmlenz@123: 'action', 'align', 'alt', 'axis', 'bgcolor', 'border', 'cellpadding', cmlenz@123: 'cellspacing', 'char', 'charoff', 'charset', 'checked', 'cite', 'class', cmlenz@123: 'clear', 'cols', 'colspan', 'color', 'compact', 'coords', 'datetime', cmlenz@123: 'dir', 'disabled', 'enctype', 'for', 'frame', 'headers', 'height', cmlenz@123: 'href', 'hreflang', 'hspace', 'id', 'ismap', 'label', 'lang', cmlenz@123: 'longdesc', 'maxlength', 'media', 'method', 'multiple', 'name', cmlenz@123: 'nohref', 'noshade', 'nowrap', 'prompt', 'readonly', 'rel', 'rev', cmlenz@123: 'rows', 'rowspan', 'rules', 'scope', 'selected', 'shape', 'size', cmlenz@431: 'span', 'src', 'start', 'summary', 'tabindex', 'target', 'title', cmlenz@431: 'type', 'usemap', 'valign', 'value', 'vspace', 'width']) cmlenz@277: hodgestar@951: SAFE_CSS = frozenset([ hodgestar@951: # CSS 3 properties hodgestar@951: 'background', 'background-attachment', 'background-color', hodgestar@951: 'background-image', 'background-position', 'background-repeat', hodgestar@951: 'border', 'border-bottom', 'border-bottom-color', hodgestar@951: 'border-bottom-style', 'border-bottom-width', 'border-collapse', hodgestar@951: 'border-color', 'border-left', 'border-left-color', hodgestar@951: 'border-left-style', 'border-left-width', 'border-right', hodgestar@951: 'border-right-color', 'border-right-style', 'border-right-width', hodgestar@951: 'border-spacing', 'border-style', 'border-top', 'border-top-color', hodgestar@951: 'border-top-style', 'border-top-width', 'border-width', 'bottom', hodgestar@951: 'caption-side', 'clear', 'clip', 'color', 'content', hodgestar@951: 'counter-increment', 'counter-reset', 'cursor', 'direction', 'display', hodgestar@951: 'empty-cells', 'float', 'font', 'font-family', 'font-size', hodgestar@951: 'font-style', 'font-variant', 'font-weight', 'height', 'left', hodgestar@951: 'letter-spacing', 'line-height', 'list-style', 'list-style-image', hodgestar@951: 'list-style-position', 'list-style-type', 'margin', 'margin-bottom', hodgestar@951: 'margin-left', 'margin-right', 'margin-top', 'max-height', 'max-width', hodgestar@951: 'min-height', 'min-width', 'opacity', 'orphans', 'outline', hodgestar@951: 'outline-color', 'outline-style', 'outline-width', 'overflow', hodgestar@951: 'padding', 'padding-bottom', 'padding-left', 'padding-right', hodgestar@951: 'padding-top', 'page-break-after', 'page-break-before', hodgestar@951: 'page-break-inside', 'quotes', 'right', 'table-layout', hodgestar@951: 'text-align', 'text-decoration', 'text-indent', 'text-transform', hodgestar@951: 'top', 'unicode-bidi', 'vertical-align', 'visibility', 'white-space', hodgestar@951: 'widows', 'width', 'word-spacing', 'z-index', hodgestar@951: ]) hodgestar@951: cmlenz@277: SAFE_SCHEMES = frozenset(['file', 'ftp', 'http', 'https', 'mailto', None]) cmlenz@277: cmlenz@277: URI_ATTRS = frozenset(['action', 'background', 'dynsrc', 'href', 'lowsrc', cmlenz@123: 'src']) cmlenz@277: cmlenz@277: def __init__(self, safe_tags=SAFE_TAGS, safe_attrs=SAFE_ATTRS, hodgestar@951: safe_schemes=SAFE_SCHEMES, uri_attrs=URI_ATTRS, hodgestar@951: safe_css=SAFE_CSS): cmlenz@277: """Create the sanitizer. cmlenz@277: cmlenz@277: The exact set of allowed elements and attributes can be configured. cmlenz@277: cmlenz@425: :param safe_tags: a set of tag names that are considered safe cmlenz@425: :param safe_attrs: a set of attribute names that are considered safe cmlenz@425: :param safe_schemes: a set of URI schemes that are considered safe cmlenz@425: :param uri_attrs: a set of names of attributes that contain URIs cmlenz@277: """ cmlenz@277: self.safe_tags = safe_tags hodgestar@951: # The set of tag names that are considered safe. cmlenz@277: self.safe_attrs = safe_attrs hodgestar@951: # The set of attribute names that are considered safe. hodgestar@951: self.safe_css = safe_css hodgestar@951: # The set of CSS properties that are considered safe. cmlenz@277: self.uri_attrs = uri_attrs hodgestar@951: # The set of names of attributes that may contain URIs. cmlenz@277: self.safe_schemes = safe_schemes hodgestar@951: # The set of URI schemes that are considered safe. hodgestar@951: hodgestar@951: # IE6 hodgestar@951: _EXPRESSION_SEARCH = re.compile(u""" hodgestar@951: [eE hodgestar@951: \uFF25 # FULLWIDTH LATIN CAPITAL LETTER E hodgestar@951: \uFF45 # FULLWIDTH LATIN SMALL LETTER E hodgestar@951: ] hodgestar@951: [xX hodgestar@951: \uFF38 # FULLWIDTH LATIN CAPITAL LETTER X hodgestar@951: \uFF58 # FULLWIDTH LATIN SMALL LETTER X hodgestar@951: ] hodgestar@951: [pP hodgestar@951: \uFF30 # FULLWIDTH LATIN CAPITAL LETTER P hodgestar@951: \uFF50 # FULLWIDTH LATIN SMALL LETTER P hodgestar@951: ] hodgestar@951: [rR hodgestar@951: \u0280 # LATIN LETTER SMALL CAPITAL R hodgestar@951: \uFF32 # FULLWIDTH LATIN CAPITAL LETTER R hodgestar@951: \uFF52 # FULLWIDTH LATIN SMALL LETTER R hodgestar@951: ] hodgestar@951: [eE hodgestar@951: \uFF25 # FULLWIDTH LATIN CAPITAL LETTER E hodgestar@951: \uFF45 # FULLWIDTH LATIN SMALL LETTER E hodgestar@951: ] hodgestar@951: [sS hodgestar@951: \uFF33 # FULLWIDTH LATIN CAPITAL LETTER S hodgestar@951: \uFF53 # FULLWIDTH LATIN SMALL LETTER S hodgestar@951: ]{2} hodgestar@951: [iI hodgestar@951: \u026A # LATIN LETTER SMALL CAPITAL I hodgestar@951: \uFF29 # FULLWIDTH LATIN CAPITAL LETTER I hodgestar@951: \uFF49 # FULLWIDTH LATIN SMALL LETTER I hodgestar@951: ] hodgestar@951: [oO hodgestar@951: \uFF2F # FULLWIDTH LATIN CAPITAL LETTER O hodgestar@951: \uFF4F # FULLWIDTH LATIN SMALL LETTER O hodgestar@951: ] hodgestar@951: [nN hodgestar@951: \u0274 # LATIN LETTER SMALL CAPITAL N hodgestar@951: \uFF2E # FULLWIDTH LATIN CAPITAL LETTER N hodgestar@951: \uFF4E # FULLWIDTH LATIN SMALL LETTER N hodgestar@951: ] hodgestar@951: """, re.VERBOSE).search hodgestar@951: hodgestar@951: # IE6 hodgestar@951: # 7) Particular bit of Unicode characters hodgestar@951: _URL_FINDITER = re.compile( hodgestar@951: u'[Uu][Rr\u0280][Ll\u029F]\s*\(([^)]+)').finditer cmlenz@123: cmlenz@439: def __call__(self, stream): cmlenz@277: """Apply the filter to the given stream. cmlenz@277: cmlenz@425: :param stream: the markup event stream to filter cmlenz@277: """ cmlenz@123: waiting_for = None cmlenz@123: cmlenz@123: for kind, data, pos in stream: cmlenz@123: if kind is START: cmlenz@123: if waiting_for: cmlenz@123: continue cmlenz@345: tag, attrs = data cmlenz@840: if not self.is_safe_elem(tag, attrs): cmlenz@123: waiting_for = tag cmlenz@123: continue cmlenz@123: cmlenz@345: new_attrs = [] cmlenz@345: for attr, value in attrs: cmlenz@123: value = stripentities(value) cmlenz@277: if attr not in self.safe_attrs: cmlenz@123: continue cmlenz@277: elif attr in self.uri_attrs: cmlenz@123: # Don't allow URI schemes such as "javascript:" cmlenz@571: if not self.is_safe_uri(value): cmlenz@123: continue cmlenz@123: elif attr == 'style': cmlenz@123: # Remove dangerous CSS declarations from inline styles cmlenz@571: decls = self.sanitize_css(value) cmlenz@123: if not decls: cmlenz@123: continue cmlenz@123: value = '; '.join(decls) cmlenz@345: new_attrs.append((attr, value)) cmlenz@123: cmlenz@345: yield kind, (tag, Attrs(new_attrs)), pos cmlenz@123: cmlenz@123: elif kind is END: cmlenz@123: tag = data cmlenz@123: if waiting_for: cmlenz@123: if waiting_for == tag: cmlenz@123: waiting_for = None cmlenz@123: else: cmlenz@123: yield kind, data, pos cmlenz@123: cmlenz@571: elif kind is not COMMENT: cmlenz@123: if not waiting_for: cmlenz@123: yield kind, data, pos cmlenz@431: cmlenz@840: def is_safe_css(self, propname, value): cmlenz@840: """Determine whether the given css property declaration is to be cmlenz@840: considered safe for inclusion in the output. cmlenz@840: cmlenz@840: :param propname: the CSS property name cmlenz@840: :param value: the value of the property cmlenz@840: :return: whether the property value should be considered safe cmlenz@840: :rtype: bool cmlenz@840: :since: version 0.6 cmlenz@840: """ hodgestar@951: if propname not in self.safe_css: cmlenz@840: return False cmlenz@840: if propname.startswith('margin') and '-' in value: cmlenz@840: # Negative margins can be used for phishing cmlenz@840: return False cmlenz@840: return True cmlenz@840: cmlenz@840: def is_safe_elem(self, tag, attrs): cmlenz@840: """Determine whether the given element should be considered safe for cmlenz@840: inclusion in the output. cmlenz@840: cmlenz@840: :param tag: the tag name of the element cmlenz@840: :type tag: QName cmlenz@840: :param attrs: the element attributes cmlenz@840: :type attrs: Attrs cmlenz@840: :return: whether the element should be considered safe cmlenz@840: :rtype: bool cmlenz@840: :since: version 0.6 cmlenz@840: """ cmlenz@840: if tag not in self.safe_tags: cmlenz@840: return False cmlenz@840: if tag.localname == 'input': cmlenz@840: input_type = attrs.get('type', '').lower() cmlenz@840: if input_type == 'password': cmlenz@840: return False cmlenz@840: return True cmlenz@840: cmlenz@571: def is_safe_uri(self, uri): cmlenz@571: """Determine whether the given URI is to be considered safe for cmlenz@571: inclusion in the output. cmlenz@571: cmlenz@571: The default implementation checks whether the scheme of the URI is in cmlenz@571: the set of allowed URIs (`safe_schemes`). cmlenz@571: cmlenz@571: >>> sanitizer = HTMLSanitizer() cmlenz@571: >>> sanitizer.is_safe_uri('http://example.org/') cmlenz@571: True cmlenz@571: >>> sanitizer.is_safe_uri('javascript:alert(document.cookie)') cmlenz@571: False cmlenz@571: cmlenz@571: :param uri: the URI to check cmlenz@571: :return: `True` if the URI can be considered safe, `False` otherwise cmlenz@571: :rtype: `bool` cmlenz@576: :since: version 0.4.3 cmlenz@571: """ cmlenz@837: if '#' in uri: cmlenz@837: uri = uri.split('#', 1)[0] # Strip out the fragment identifier cmlenz@571: if ':' not in uri: cmlenz@571: return True # This is a relative URI cmlenz@571: chars = [char for char in uri.split(':', 1)[0] if char.isalnum()] cmlenz@571: return ''.join(chars).lower() in self.safe_schemes cmlenz@571: cmlenz@571: def sanitize_css(self, text): cmlenz@571: """Remove potentially dangerous property declarations from CSS code. cmlenz@571: cmlenz@571: In particular, properties using the CSS ``url()`` function with a scheme cmlenz@571: that is not considered safe are removed: cmlenz@571: cmlenz@571: >>> sanitizer = HTMLSanitizer() cmlenz@571: >>> sanitizer.sanitize_css(u''' cmlenz@571: ... background: url(javascript:alert("foo")); cmlenz@571: ... color: #000; cmlenz@571: ... ''') cmlenz@571: [u'color: #000'] cmlenz@571: cmlenz@571: Also, the proprietary Internet Explorer function ``expression()`` is cmlenz@571: always stripped: cmlenz@571: cmlenz@571: >>> sanitizer.sanitize_css(u''' cmlenz@571: ... background: #fff; cmlenz@571: ... color: #000; cmlenz@571: ... width: e/**/xpression(alert("foo")); cmlenz@571: ... ''') cmlenz@571: [u'background: #fff', u'color: #000'] cmlenz@571: cmlenz@571: :param text: the CSS text; this is expected to be `unicode` and to not cmlenz@571: contain any character or numeric references cmlenz@571: :return: a list of declarations that are considered safe cmlenz@571: :rtype: `list` cmlenz@576: :since: version 0.4.3 cmlenz@571: """ cmlenz@571: decls = [] cmlenz@571: text = self._strip_css_comments(self._replace_unicode_escapes(text)) cmlenz@856: for decl in text.split(';'): cmlenz@571: decl = decl.strip() cmlenz@571: if not decl: cmlenz@571: continue cmlenz@840: try: cmlenz@840: propname, value = decl.split(':', 1) cmlenz@840: except ValueError: cmlenz@840: continue cmlenz@840: if not self.is_safe_css(propname.strip().lower(), value.strip()): cmlenz@840: continue cmlenz@571: is_evil = False hodgestar@951: if self._EXPRESSION_SEARCH(value): cmlenz@571: is_evil = True hodgestar@951: for match in self._URL_FINDITER(value): cmlenz@571: if not self.is_safe_uri(match.group(1)): cmlenz@571: is_evil = True cmlenz@571: break cmlenz@571: if not is_evil: cmlenz@571: decls.append(decl.strip()) cmlenz@571: return decls cmlenz@571: cmlenz@431: _NORMALIZE_NEWLINES = re.compile(r'\r\n').sub hodgestar@951: _UNICODE_ESCAPE = re.compile( hodgestar@951: r"""\\([0-9a-fA-F]{1,6})\s?|\\([^\r\n\f0-9a-fA-F'"{};:()#*])""", hodgestar@951: re.UNICODE).sub cmlenz@431: cmlenz@431: def _replace_unicode_escapes(self, text): cmlenz@431: def _repl(match): hodgestar@951: t = match.group(1) hodgestar@951: if t: hodgestar@951: return unichr(int(t, 16)) hodgestar@951: t = match.group(2) hodgestar@951: if t == '\\': hodgestar@951: return r'\\' hodgestar@951: else: hodgestar@951: return t cmlenz@431: return self._UNICODE_ESCAPE(_repl, self._NORMALIZE_NEWLINES('\n', text)) cmlenz@556: cmlenz@556: _CSS_COMMENTS = re.compile(r'/\*.*?\*/').sub cmlenz@556: cmlenz@556: def _strip_css_comments(self, text): cmlenz@556: return self._CSS_COMMENTS('', text)