1 | """ |
---|
2 | HTML Sanitizer (ripped from feedparser) |
---|
3 | """ |
---|
4 | |
---|
5 | import re, sgmllib |
---|
6 | |
---|
7 | # reversable htmlentitydefs mappings for Python 2.2 |
---|
8 | try: |
---|
9 | from htmlentitydefs import name2codepoint, codepoint2name |
---|
10 | except: |
---|
11 | import htmlentitydefs |
---|
12 | name2codepoint={} |
---|
13 | codepoint2name={} |
---|
14 | for (name,codepoint) in htmlentitydefs.entitydefs.iteritems(): |
---|
15 | if codepoint.startswith('&#'): codepoint=unichr(int(codepoint[2:-1])) |
---|
16 | name2codepoint[name]=ord(codepoint) |
---|
17 | codepoint2name[ord(codepoint)]=name |
---|
18 | |
---|
19 | _cp1252 = { |
---|
20 | unichr(128): unichr(8364), # euro sign |
---|
21 | unichr(130): unichr(8218), # single low-9 quotation mark |
---|
22 | unichr(131): unichr( 402), # latin small letter f with hook |
---|
23 | unichr(132): unichr(8222), # double low-9 quotation mark |
---|
24 | unichr(133): unichr(8230), # horizontal ellipsis |
---|
25 | unichr(134): unichr(8224), # dagger |
---|
26 | unichr(135): unichr(8225), # double dagger |
---|
27 | unichr(136): unichr( 710), # modifier letter circumflex accent |
---|
28 | unichr(137): unichr(8240), # per mille sign |
---|
29 | unichr(138): unichr( 352), # latin capital letter s with caron |
---|
30 | unichr(139): unichr(8249), # single left-pointing angle quotation mark |
---|
31 | unichr(140): unichr( 338), # latin capital ligature oe |
---|
32 | unichr(142): unichr( 381), # latin capital letter z with caron |
---|
33 | unichr(145): unichr(8216), # left single quotation mark |
---|
34 | unichr(146): unichr(8217), # right single quotation mark |
---|
35 | unichr(147): unichr(8220), # left double quotation mark |
---|
36 | unichr(148): unichr(8221), # right double quotation mark |
---|
37 | unichr(149): unichr(8226), # bullet |
---|
38 | unichr(150): unichr(8211), # en dash |
---|
39 | unichr(151): unichr(8212), # em dash |
---|
40 | unichr(152): unichr( 732), # small tilde |
---|
41 | unichr(153): unichr(8482), # trade mark sign |
---|
42 | unichr(154): unichr( 353), # latin small letter s with caron |
---|
43 | unichr(155): unichr(8250), # single right-pointing angle quotation mark |
---|
44 | unichr(156): unichr( 339), # latin small ligature oe |
---|
45 | unichr(158): unichr( 382), # latin small letter z with caron |
---|
46 | unichr(159): unichr( 376)} # latin capital letter y with diaeresis |
---|
47 | |
---|
48 | class _BaseHTMLProcessor(sgmllib.SGMLParser): |
---|
49 | special = re.compile('''[<>'"]''') |
---|
50 | bare_ampersand = re.compile("&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)") |
---|
51 | elements_no_end_tag = ['area', 'base', 'basefont', 'br', 'col', 'frame', 'hr', |
---|
52 | 'img', 'input', 'isindex', 'link', 'meta', 'param'] |
---|
53 | |
---|
54 | def __init__(self, encoding, type): |
---|
55 | self.encoding = encoding |
---|
56 | self.type = type |
---|
57 | ## if _debug: sys.stderr.write('entering BaseHTMLProcessor, encoding=%s\n' % self.encoding) |
---|
58 | sgmllib.SGMLParser.__init__(self) |
---|
59 | |
---|
60 | def reset(self): |
---|
61 | self.pieces = [] |
---|
62 | sgmllib.SGMLParser.reset(self) |
---|
63 | |
---|
64 | def _shorttag_replace(self, match): |
---|
65 | tag = match.group(1) |
---|
66 | if tag in self.elements_no_end_tag: |
---|
67 | return '<' + tag + ' />' |
---|
68 | else: |
---|
69 | return '<' + tag + '></' + tag + '>' |
---|
70 | |
---|
71 | def parse_starttag(self,i): |
---|
72 | j=sgmllib.SGMLParser.parse_starttag(self, i) |
---|
73 | if self.type == 'application/xhtml+xml': |
---|
74 | if j>2 and self.rawdata[j-2:j]=='/>': |
---|
75 | self.unknown_endtag(self.lasttag) |
---|
76 | return j |
---|
77 | |
---|
78 | def feed(self, data): |
---|
79 | data = re.compile(r'<!((?!DOCTYPE|--|\[))', re.IGNORECASE).sub(r'<!\1', data) |
---|
80 | #data = re.sub(r'<(\S+?)\s*?/>', self._shorttag_replace, data) # bug [ 1399464 ] Bad regexp for _shorttag_replace |
---|
81 | data = re.sub(r'<([^<>\s]+?)\s*/>', self._shorttag_replace, data) |
---|
82 | data = data.replace(''', "'") |
---|
83 | data = data.replace('"', '"') |
---|
84 | if self.encoding and type(data) == type(u''): |
---|
85 | data = data.encode(self.encoding) |
---|
86 | sgmllib.SGMLParser.feed(self, data) |
---|
87 | sgmllib.SGMLParser.close(self) |
---|
88 | |
---|
89 | def normalize_attrs(self, attrs): |
---|
90 | if not attrs: return attrs |
---|
91 | # utility method to be called by descendants |
---|
92 | attrs = dict([(k.lower(), v) for k, v in attrs]).items() |
---|
93 | attrs = [(k, k in ('rel', 'type') and v.lower() or v) for k, v in attrs] |
---|
94 | attrs.sort() |
---|
95 | return attrs |
---|
96 | |
---|
97 | def unknown_starttag(self, tag, attrs): |
---|
98 | # called for each start tag |
---|
99 | # attrs is a list of (attr, value) tuples |
---|
100 | # e.g. for <pre class='screen'>, tag='pre', attrs=[('class', 'screen')] |
---|
101 | ## if _debug: sys.stderr.write('_BaseHTMLProcessor, unknown_starttag, tag=%s\n' % tag) |
---|
102 | uattrs = [] |
---|
103 | strattrs='' |
---|
104 | if attrs: |
---|
105 | for key, value in attrs: |
---|
106 | value=value.replace('>','>').replace('<','<').replace('"','"') |
---|
107 | value = self.bare_ampersand.sub("&", value) |
---|
108 | # thanks to Kevin Marks for this breathtaking hack to deal with (valid) high-bit attribute values in UTF-8 feeds |
---|
109 | if type(value) != type(u''): |
---|
110 | try: |
---|
111 | value = unicode(value, self.encoding) |
---|
112 | except: |
---|
113 | value = unicode(value, 'iso-8859-1') |
---|
114 | uattrs.append((unicode(key, self.encoding), value)) |
---|
115 | strattrs = u''.join([u' %s="%s"' % (key, value) for key, value in uattrs]) |
---|
116 | if self.encoding: |
---|
117 | try: |
---|
118 | strattrs=strattrs.encode(self.encoding) |
---|
119 | except: |
---|
120 | pass |
---|
121 | if tag in self.elements_no_end_tag: |
---|
122 | self.pieces.append('<%(tag)s%(strattrs)s />' % locals()) |
---|
123 | else: |
---|
124 | self.pieces.append('<%(tag)s%(strattrs)s>' % locals()) |
---|
125 | |
---|
126 | def unknown_endtag(self, tag): |
---|
127 | # called for each end tag, e.g. for </pre>, tag will be 'pre' |
---|
128 | # Reconstruct the original end tag. |
---|
129 | if tag not in self.elements_no_end_tag: |
---|
130 | self.pieces.append("</%(tag)s>" % locals()) |
---|
131 | |
---|
132 | def handle_charref(self, ref): |
---|
133 | # called for each character reference, e.g. for ' ', ref will be '160' |
---|
134 | # Reconstruct the original character reference. |
---|
135 | if ref.startswith('x'): |
---|
136 | value = unichr(int(ref[1:],16)) |
---|
137 | else: |
---|
138 | value = unichr(int(ref)) |
---|
139 | |
---|
140 | if value in _cp1252.keys(): |
---|
141 | self.pieces.append('&#%s;' % hex(ord(_cp1252[value]))[1:]) |
---|
142 | else: |
---|
143 | self.pieces.append('&#%(ref)s;' % locals()) |
---|
144 | |
---|
145 | def handle_entityref(self, ref): |
---|
146 | # called for each entity reference, e.g. for '©', ref will be 'copy' |
---|
147 | # Reconstruct the original entity reference. |
---|
148 | if name2codepoint.has_key(ref): |
---|
149 | self.pieces.append('&%(ref)s;' % locals()) |
---|
150 | else: |
---|
151 | self.pieces.append('&%(ref)s' % locals()) |
---|
152 | |
---|
153 | def handle_data(self, text): |
---|
154 | # called for each block of plain text, i.e. outside of any tag and |
---|
155 | # not containing any character or entity references |
---|
156 | # Store the original text verbatim. |
---|
157 | ## if _debug: sys.stderr.write('_BaseHTMLProcessor, handle_text, text=%s\n' % text) |
---|
158 | self.pieces.append(text) |
---|
159 | |
---|
160 | def handle_comment(self, text): |
---|
161 | # called for each HTML comment, e.g. <!-- insert Javascript code here --> |
---|
162 | # Reconstruct the original comment. |
---|
163 | self.pieces.append('<!--%(text)s-->' % locals()) |
---|
164 | |
---|
165 | def handle_pi(self, text): |
---|
166 | # called for each processing instruction, e.g. <?instruction> |
---|
167 | # Reconstruct original processing instruction. |
---|
168 | self.pieces.append('<?%(text)s>' % locals()) |
---|
169 | |
---|
170 | def handle_decl(self, text): |
---|
171 | # called for the DOCTYPE, if present, e.g. |
---|
172 | # <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" |
---|
173 | # "http://www.w3.org/TR/html4/loose.dtd"> |
---|
174 | # Reconstruct original DOCTYPE |
---|
175 | self.pieces.append('<!%(text)s>' % locals()) |
---|
176 | |
---|
177 | _new_declname_match = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9:]*\s*').match |
---|
178 | def _scan_name(self, i, declstartpos): |
---|
179 | rawdata = self.rawdata |
---|
180 | n = len(rawdata) |
---|
181 | if i == n: |
---|
182 | return None, -1 |
---|
183 | m = self._new_declname_match(rawdata, i) |
---|
184 | if m: |
---|
185 | s = m.group() |
---|
186 | name = s.strip() |
---|
187 | if (i + len(s)) == n: |
---|
188 | return None, -1 # end of buffer |
---|
189 | return name.lower(), m.end() |
---|
190 | else: |
---|
191 | self.handle_data(rawdata) |
---|
192 | # self.updatepos(declstartpos, i) |
---|
193 | return None, -1 |
---|
194 | |
---|
195 | def convert_charref(self, name): |
---|
196 | return '&#%s;' % name |
---|
197 | |
---|
198 | def convert_entityref(self, name): |
---|
199 | return '&%s;' % name |
---|
200 | |
---|
201 | def output(self): |
---|
202 | '''Return processed HTML as a single string''' |
---|
203 | return ''.join([str(p) for p in self.pieces]) |
---|
204 | |
---|
205 | class _HTMLSanitizer(_BaseHTMLProcessor): |
---|
206 | acceptable_elements = ['a', 'abbr', 'acronym', 'address', 'area', 'article', |
---|
207 | 'aside', 'audio', 'b', 'big', 'blockquote', 'br', 'button', 'canvas', |
---|
208 | 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'command', |
---|
209 | 'datagrid', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', |
---|
210 | 'div', 'dl', 'dt', 'em', 'event-source', 'fieldset', 'figure', 'footer', |
---|
211 | 'font', 'form', 'header', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', |
---|
212 | 'img', 'input', 'ins', 'keygen', 'kbd', 'label', 'legend', 'li', 'm', 'map', |
---|
213 | 'menu', 'meter', 'multicol', 'nav', 'nextid', 'ol', 'output', 'optgroup', |
---|
214 | 'option', 'p', 'pre', 'progress', 'q', 's', 'samp', 'section', 'select', |
---|
215 | 'small', 'sound', 'source', 'spacer', 'span', 'strike', 'strong', 'sub', |
---|
216 | 'sup', 'table', 'tbody', 'td', 'textarea', 'time', 'tfoot', 'th', 'thead', |
---|
217 | 'tr', 'tt', 'u', 'ul', 'var', 'video', 'noscript'] |
---|
218 | |
---|
219 | acceptable_attributes = ['abbr', 'accept', 'accept-charset', 'accesskey', |
---|
220 | 'action', 'align', 'alt', 'autocomplete', 'autofocus', 'axis', |
---|
221 | 'background', 'balance', 'bgcolor', 'bgproperties', 'border', |
---|
222 | 'bordercolor', 'bordercolordark', 'bordercolorlight', 'bottompadding', |
---|
223 | 'cellpadding', 'cellspacing', 'ch', 'challenge', 'char', 'charoff', |
---|
224 | 'choff', 'charset', 'checked', 'cite', 'class', 'clear', 'color', 'cols', |
---|
225 | 'colspan', 'compact', 'contenteditable', 'controls', 'coords', 'data', |
---|
226 | 'datafld', 'datapagesize', 'datasrc', 'datetime', 'default', 'delay', |
---|
227 | 'dir', 'disabled', 'draggable', 'dynsrc', 'enctype', 'end', 'face', 'for', |
---|
228 | 'form', 'frame', 'galleryimg', 'gutter', 'headers', 'height', 'hidefocus', |
---|
229 | 'hidden', 'high', 'href', 'hreflang', 'hspace', 'icon', 'id', 'inputmode', |
---|
230 | 'ismap', 'keytype', 'label', 'leftspacing', 'lang', 'list', 'longdesc', |
---|
231 | 'loop', 'loopcount', 'loopend', 'loopstart', 'low', 'lowsrc', 'max', |
---|
232 | 'maxlength', 'media', 'method', 'min', 'multiple', 'name', 'nohref', |
---|
233 | 'noshade', 'nowrap', 'open', 'optimum', 'pattern', 'ping', 'point-size', |
---|
234 | 'prompt', 'pqg', 'radiogroup', 'readonly', 'rel', 'repeat-max', |
---|
235 | 'repeat-min', 'replace', 'required', 'rev', 'rightspacing', 'rows', |
---|
236 | 'rowspan', 'rules', 'scope', 'selected', 'shape', 'size', 'span', 'src', |
---|
237 | 'start', 'step', 'summary', 'suppress', 'tabindex', 'target', 'template', |
---|
238 | 'title', 'toppadding', 'type', 'unselectable', 'usemap', 'urn', 'valign', |
---|
239 | 'value', 'variable', 'volume', 'vspace', 'vrml', 'width', 'wrap', |
---|
240 | 'xml:lang'] |
---|
241 | |
---|
242 | unacceptable_elements_with_end_tag = ['script', 'applet', 'style'] |
---|
243 | |
---|
244 | acceptable_css_properties = ['azimuth', 'background-color', |
---|
245 | 'border-bottom-color', 'border-collapse', 'border-color', |
---|
246 | 'border-left-color', 'border-right-color', 'border-top-color', 'clear', |
---|
247 | 'color', 'cursor', 'direction', 'display', 'elevation', 'float', 'font', |
---|
248 | 'font-family', 'font-size', 'font-style', 'font-variant', 'font-weight', |
---|
249 | 'height', 'letter-spacing', 'line-height', 'overflow', 'pause', |
---|
250 | 'pause-after', 'pause-before', 'pitch', 'pitch-range', 'richness', |
---|
251 | 'speak', 'speak-header', 'speak-numeral', 'speak-punctuation', |
---|
252 | 'speech-rate', 'stress', 'text-align', 'text-decoration', 'text-indent', |
---|
253 | 'unicode-bidi', 'vertical-align', 'voice-family', 'volume', |
---|
254 | 'white-space', 'width'] |
---|
255 | |
---|
256 | # survey of common keywords found in feeds |
---|
257 | acceptable_css_keywords = ['auto', 'aqua', 'black', 'block', 'blue', |
---|
258 | 'bold', 'both', 'bottom', 'brown', 'center', 'collapse', 'dashed', |
---|
259 | 'dotted', 'fuchsia', 'gray', 'green', '!important', 'italic', 'left', |
---|
260 | 'lime', 'maroon', 'medium', 'none', 'navy', 'normal', 'nowrap', 'olive', |
---|
261 | 'pointer', 'purple', 'red', 'right', 'solid', 'silver', 'teal', 'top', |
---|
262 | 'transparent', 'underline', 'white', 'yellow'] |
---|
263 | |
---|
264 | valid_css_values = re.compile('^(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|' + |
---|
265 | '\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$') |
---|
266 | |
---|
267 | mathml_elements = ['annotation', 'annotation-xml', 'maction', 'math', |
---|
268 | 'merror', 'mfenced', 'mfrac', 'mi', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', |
---|
269 | 'mphantom', 'mprescripts', 'mroot', 'mrow', 'mspace', 'msqrt', 'mstyle', |
---|
270 | 'msub', 'msubsup', 'msup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', |
---|
271 | 'munderover', 'none', 'semantics'] |
---|
272 | |
---|
273 | mathml_attributes = ['actiontype', 'align', 'columnalign', 'columnalign', |
---|
274 | 'columnalign', 'close', 'columnlines', 'columnspacing', 'columnspan', 'depth', |
---|
275 | 'display', 'displaystyle', 'encoding', 'equalcolumns', 'equalrows', |
---|
276 | 'fence', 'fontstyle', 'fontweight', 'frame', 'height', 'linethickness', |
---|
277 | 'lspace', 'mathbackground', 'mathcolor', 'mathvariant', 'mathvariant', |
---|
278 | 'maxsize', 'minsize', 'open', 'other', 'rowalign', 'rowalign', 'rowalign', |
---|
279 | 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'scriptlevel', 'selection', |
---|
280 | 'separator', 'separators', 'stretchy', 'width', 'width', 'xlink:href', |
---|
281 | 'xlink:show', 'xlink:type', 'xmlns', 'xmlns:xlink'] |
---|
282 | |
---|
283 | # svgtiny - foreignObject + linearGradient + radialGradient + stop |
---|
284 | svg_elements = ['a', 'animate', 'animateColor', 'animateMotion', |
---|
285 | 'animateTransform', 'circle', 'defs', 'desc', 'ellipse', 'foreignObject', |
---|
286 | 'font-face', 'font-face-name', 'font-face-src', 'g', 'glyph', 'hkern', |
---|
287 | 'linearGradient', 'line', 'marker', 'metadata', 'missing-glyph', 'mpath', |
---|
288 | 'path', 'polygon', 'polyline', 'radialGradient', 'rect', 'set', 'stop', |
---|
289 | 'svg', 'switch', 'text', 'title', 'tspan', 'use'] |
---|
290 | |
---|
291 | # svgtiny + class + opacity + offset + xmlns + xmlns:xlink |
---|
292 | svg_attributes = ['accent-height', 'accumulate', 'additive', 'alphabetic', |
---|
293 | 'arabic-form', 'ascent', 'attributeName', 'attributeType', |
---|
294 | 'baseProfile', 'bbox', 'begin', 'by', 'calcMode', 'cap-height', |
---|
295 | 'class', 'color', 'color-rendering', 'content', 'cx', 'cy', 'd', 'dx', |
---|
296 | 'dy', 'descent', 'display', 'dur', 'end', 'fill', 'fill-opacity', |
---|
297 | 'fill-rule', 'font-family', 'font-size', 'font-stretch', 'font-style', |
---|
298 | 'font-variant', 'font-weight', 'from', 'fx', 'fy', 'g1', 'g2', |
---|
299 | 'glyph-name', 'gradientUnits', 'hanging', 'height', 'horiz-adv-x', |
---|
300 | 'horiz-origin-x', 'id', 'ideographic', 'k', 'keyPoints', 'keySplines', |
---|
301 | 'keyTimes', 'lang', 'mathematical', 'marker-end', 'marker-mid', |
---|
302 | 'marker-start', 'markerHeight', 'markerUnits', 'markerWidth', 'max', |
---|
303 | 'min', 'name', 'offset', 'opacity', 'orient', 'origin', |
---|
304 | 'overline-position', 'overline-thickness', 'panose-1', 'path', |
---|
305 | 'pathLength', 'points', 'preserveAspectRatio', 'r', 'refX', 'refY', |
---|
306 | 'repeatCount', 'repeatDur', 'requiredExtensions', 'requiredFeatures', |
---|
307 | 'restart', 'rotate', 'rx', 'ry', 'slope', 'stemh', 'stemv', |
---|
308 | 'stop-color', 'stop-opacity', 'strikethrough-position', |
---|
309 | 'strikethrough-thickness', 'stroke', 'stroke-dasharray', |
---|
310 | 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', |
---|
311 | 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', |
---|
312 | 'target', 'text-anchor', 'to', 'transform', 'type', 'u1', 'u2', |
---|
313 | 'underline-position', 'underline-thickness', 'unicode', 'unicode-range', |
---|
314 | 'units-per-em', 'values', 'version', 'viewBox', 'visibility', 'width', |
---|
315 | 'widths', 'x', 'x-height', 'x1', 'x2', 'xlink:actuate', 'xlink:arcrole', |
---|
316 | 'xlink:href', 'xlink:role', 'xlink:show', 'xlink:title', 'xlink:type', |
---|
317 | 'xml:base', 'xml:lang', 'xml:space', 'xmlns', 'xmlns:xlink', 'y', 'y1', |
---|
318 | 'y2', 'zoomAndPan'] |
---|
319 | |
---|
320 | svg_attr_map = None |
---|
321 | svg_elem_map = None |
---|
322 | |
---|
323 | acceptable_svg_properties = [ 'fill', 'fill-opacity', 'fill-rule', |
---|
324 | 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', |
---|
325 | 'stroke-opacity'] |
---|
326 | |
---|
327 | def reset(self): |
---|
328 | _BaseHTMLProcessor.reset(self) |
---|
329 | self.unacceptablestack = 0 |
---|
330 | self.mathmlOK = 0 |
---|
331 | self.svgOK = 0 |
---|
332 | |
---|
333 | def unknown_starttag(self, tag, attrs): |
---|
334 | acceptable_attributes = self.acceptable_attributes |
---|
335 | keymap = {} |
---|
336 | if not tag in self.acceptable_elements or self.svgOK: |
---|
337 | if tag in self.unacceptable_elements_with_end_tag: |
---|
338 | self.unacceptablestack += 1 |
---|
339 | |
---|
340 | # not otherwise acceptable, perhaps it is MathML or SVG? |
---|
341 | if tag=='math' and ('xmlns','http://www.w3.org/1998/Math/MathML') in attrs: |
---|
342 | self.mathmlOK += 1 |
---|
343 | if tag=='svg' and ('xmlns','http://www.w3.org/2000/svg') in attrs: |
---|
344 | self.svgOK += 1 |
---|
345 | |
---|
346 | # chose acceptable attributes based on tag class, else bail |
---|
347 | if self.mathmlOK and tag in self.mathml_elements: |
---|
348 | acceptable_attributes = self.mathml_attributes |
---|
349 | elif self.svgOK and tag in self.svg_elements: |
---|
350 | # for most vocabularies, lowercasing is a good idea. Many |
---|
351 | # svg elements, however, are camel case |
---|
352 | if not self.svg_attr_map: |
---|
353 | lower=[attr.lower() for attr in self.svg_attributes] |
---|
354 | mix=[a for a in self.svg_attributes if a not in lower] |
---|
355 | self.svg_attributes = lower |
---|
356 | self.svg_attr_map = dict([(a.lower(),a) for a in mix]) |
---|
357 | |
---|
358 | lower=[attr.lower() for attr in self.svg_elements] |
---|
359 | mix=[a for a in self.svg_elements if a not in lower] |
---|
360 | self.svg_elements = lower |
---|
361 | self.svg_elem_map = dict([(a.lower(),a) for a in mix]) |
---|
362 | acceptable_attributes = self.svg_attributes |
---|
363 | tag = self.svg_elem_map.get(tag,tag) |
---|
364 | keymap = self.svg_attr_map |
---|
365 | elif not tag in self.acceptable_elements: |
---|
366 | return |
---|
367 | |
---|
368 | # declare xlink namespace, if needed |
---|
369 | if self.mathmlOK or self.svgOK: |
---|
370 | if filter(lambda (n,v): n.startswith('xlink:'),attrs): |
---|
371 | if not ('xmlns:xlink','http://www.w3.org/1999/xlink') in attrs: |
---|
372 | attrs.append(('xmlns:xlink','http://www.w3.org/1999/xlink')) |
---|
373 | |
---|
374 | clean_attrs = [] |
---|
375 | for key, value in self.normalize_attrs(attrs): |
---|
376 | if key in acceptable_attributes: |
---|
377 | key=keymap.get(key,key) |
---|
378 | clean_attrs.append((key,value)) |
---|
379 | elif key=='style': |
---|
380 | pass |
---|
381 | ## clean_value = self.sanitize_style(value) |
---|
382 | ## if clean_value: clean_attrs.append((key,clean_value)) |
---|
383 | _BaseHTMLProcessor.unknown_starttag(self, tag, clean_attrs) |
---|
384 | |
---|
385 | def unknown_endtag(self, tag): |
---|
386 | if not tag in self.acceptable_elements: |
---|
387 | if tag in self.unacceptable_elements_with_end_tag: |
---|
388 | self.unacceptablestack -= 1 |
---|
389 | if self.mathmlOK and tag in self.mathml_elements: |
---|
390 | if tag == 'math' and self.mathmlOK: self.mathmlOK -= 1 |
---|
391 | elif self.svgOK and tag in self.svg_elements: |
---|
392 | tag = self.svg_elem_map.get(tag,tag) |
---|
393 | if tag == 'svg' and self.svgOK: self.svgOK -= 1 |
---|
394 | else: |
---|
395 | return |
---|
396 | _BaseHTMLProcessor.unknown_endtag(self, tag) |
---|
397 | |
---|
398 | def handle_pi(self, text): |
---|
399 | pass |
---|
400 | |
---|
401 | def handle_decl(self, text): |
---|
402 | pass |
---|
403 | |
---|
404 | def handle_data(self, text): |
---|
405 | if not self.unacceptablestack: |
---|
406 | _BaseHTMLProcessor.handle_data(self, text) |
---|
407 | |
---|
408 | def sanitize_style(self, style): |
---|
409 | # disallow urls |
---|
410 | style=re.compile('url\s*\(\s*[^\s)]+?\s*\)\s*').sub(' ',style) |
---|
411 | |
---|
412 | # gauntlet |
---|
413 | if not re.match("""^([:,;#%.\sa-zA-Z0-9!]|\w-\w|'[\s\w]+'|"[\s\w]+"|\([\d,\s]+\))*$""", style): return '' |
---|
414 | if not re.match("^(\s*[-\w]+\s*:\s*[^:;]*(;|$))*$", style): return '' |
---|
415 | |
---|
416 | clean = [] |
---|
417 | for prop,value in re.findall("([-\w]+)\s*:\s*([^:;]*)",style): |
---|
418 | if not value: continue |
---|
419 | if prop.lower() in self.acceptable_css_properties: |
---|
420 | clean.append(prop + ': ' + value + ';') |
---|
421 | elif prop.split('-')[0].lower() in ['background','border','margin','padding']: |
---|
422 | for keyword in value.split(): |
---|
423 | if not keyword in self.acceptable_css_keywords and \ |
---|
424 | not self.valid_css_values.match(keyword): |
---|
425 | break |
---|
426 | else: |
---|
427 | clean.append(prop + ': ' + value + ';') |
---|
428 | elif self.svgOK and prop.lower() in self.acceptable_svg_properties: |
---|
429 | clean.append(prop + ': ' + value + ';') |
---|
430 | |
---|
431 | return ' '.join(clean) |
---|
432 | |
---|
433 | |
---|
434 | def sanitize_html(htmlSource, encoding, type): |
---|
435 | p = _HTMLSanitizer(encoding, type) |
---|
436 | p.feed(htmlSource) |
---|
437 | data = p.output() |
---|
438 | data = data.strip().replace('\r\n', '\n') |
---|
439 | return data |
---|