1 # Copyright 2010-2023 Kurt McKee <contactme@kurtmckee.org> 2 # Copyright 2002-2008 Mark Pilgrim 3 # All rights reserved. 4 # 5 # This file is a part of feedparser. 6 # 7 # Redistribution and use in source and binary forms, with or without 8 # modification, are permitted provided that the following conditions are met: 9 # 10 # * Redistributions of source code must retain the above copyright notice, 11 # this list of conditions and the following disclaimer. 12 # * Redistributions in binary form must reproduce the above copyright notice, 13 # this list of conditions and the following disclaimer in the documentation 14 # and/or other materials provided with the distribution. 15 # 16 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS' 17 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 20 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 # POSSIBILITY OF SUCH DAMAGE. 27 28 import re 29 30 from .html import _BaseHTMLProcessor 31 from .urls import make_safe_absolute_uri 32 33 34 class _HTMLSanitizer(_BaseHTMLProcessor): 35 acceptable_elements = { 36 'a', 37 'abbr', 38 'acronym', 39 'address', 40 'area', 41 'article', 42 'aside', 43 'audio', 44 'b', 45 'big', 46 'blockquote', 47 'br', 48 'button', 49 'canvas', 50 'caption', 51 'center', 52 'cite', 53 'code', 54 'col', 55 'colgroup', 56 'command', 57 'datagrid', 58 'datalist', 59 'dd', 60 'del', 61 'details', 62 'dfn', 63 'dialog', 64 'dir', 65 'div', 66 'dl', 67 'dt', 68 'em', 69 'event-source', 70 'fieldset', 71 'figcaption', 72 'figure', 73 'font', 74 'footer', 75 'form', 76 'h1', 77 'h2', 78 'h3', 79 'h4', 80 'h5', 81 'h6', 82 'header', 83 'hr', 84 'i', 85 'img', 86 'input', 87 'ins', 88 'kbd', 89 'keygen', 90 'label', 91 'legend', 92 'li', 93 'm', 94 'map', 95 'menu', 96 'meter', 97 'multicol', 98 'nav', 99 'nextid', 100 'noscript', 101 'ol', 102 'optgroup', 103 'option', 104 'output', 105 'p', 106 'pre', 107 'progress', 108 'q', 109 's', 110 'samp', 111 'section', 112 'select', 113 'small', 114 'sound', 115 'source', 116 'spacer', 117 'span', 118 'strike', 119 'strong', 120 'sub', 121 'sup', 122 'table', 123 'tbody', 124 'td', 125 'textarea', 126 'tfoot', 127 'th', 128 'thead', 129 'time', 130 'tr', 131 'tt', 132 'u', 133 'ul', 134 'var', 135 'video', 136 } 137 138 acceptable_attributes = { 139 'abbr', 140 'accept', 141 'accept-charset', 142 'accesskey', 143 'action', 144 'align', 145 'alt', 146 'autocomplete', 147 'autofocus', 148 'axis', 149 'background', 150 'balance', 151 'bgcolor', 152 'bgproperties', 153 'border', 154 'bordercolor', 155 'bordercolordark', 156 'bordercolorlight', 157 'bottompadding', 158 'cellpadding', 159 'cellspacing', 160 'ch', 161 'challenge', 162 'char', 163 'charoff', 164 'charset', 165 'checked', 166 'choff', 167 'cite', 168 'class', 169 'clear', 170 'color', 171 'cols', 172 'colspan', 173 'compact', 174 'contenteditable', 175 'controls', 176 'coords', 177 'data', 178 'datafld', 179 'datapagesize', 180 'datasrc', 181 'datetime', 182 'default', 183 'delay', 184 'dir', 185 'disabled', 186 'draggable', 187 'dynsrc', 188 'enctype', 189 'end', 190 'face', 191 'for', 192 'form', 193 'frame', 194 'galleryimg', 195 'gutter', 196 'headers', 197 'height', 198 'hidden', 199 'hidefocus', 200 'high', 201 'href', 202 'hreflang', 203 'hspace', 204 'icon', 205 'id', 206 'inputmode', 207 'ismap', 208 'keytype', 209 'label', 210 'lang', 211 'leftspacing', 212 'list', 213 'longdesc', 214 'loop', 215 'loopcount', 216 'loopend', 217 'loopstart', 218 'low', 219 'lowsrc', 220 'max', 221 'maxlength', 222 'media', 223 'method', 224 'min', 225 'multiple', 226 'name', 227 'nohref', 228 'noshade', 229 'nowrap', 230 'open', 231 'optimum', 232 'pattern', 233 'ping', 234 'point-size', 235 'poster', 236 'pqg', 237 'preload', 238 'prompt', 239 'radiogroup', 240 'readonly', 241 'rel', 242 'repeat-max', 243 'repeat-min', 244 'replace', 245 'required', 246 'rev', 247 'rightspacing', 248 'rows', 249 'rowspan', 250 'rules', 251 'scope', 252 'selected', 253 'shape', 254 'size', 255 'span', 256 'src', 257 'start', 258 'step', 259 'style', 260 'summary', 261 'suppress', 262 'tabindex', 263 'target', 264 'template', 265 'title', 266 'toppadding', 267 'type', 268 'unselectable', 269 'urn', 270 'usemap', 271 'valign', 272 'value', 273 'variable', 274 'volume', 275 'vrml', 276 'vspace', 277 'width', 278 'wrap', 279 'xml:lang', 280 } 281 282 unacceptable_elements_with_end_tag = { 283 'applet', 284 'script', 285 'style', 286 } 287 288 acceptable_css_properties = { 289 'azimuth', 290 'background-color', 291 'border-bottom-color', 292 'border-collapse', 293 'border-color', 294 'border-left-color', 295 'border-right-color', 296 'border-top-color', 297 'clear', 298 'color', 299 'cursor', 300 'direction', 301 'display', 302 'elevation', 303 'float', 304 'font', 305 'font-family', 306 'font-size', 307 'font-style', 308 'font-variant', 309 'font-weight', 310 'height', 311 'letter-spacing', 312 'line-height', 313 'overflow', 314 'pause', 315 'pause-after', 316 'pause-before', 317 'pitch', 318 'pitch-range', 319 'richness', 320 'speak', 321 'speak-header', 322 'speak-numeral', 323 'speak-punctuation', 324 'speech-rate', 325 'stress', 326 'text-align', 327 'text-decoration', 328 'text-indent', 329 'unicode-bidi', 330 'vertical-align', 331 'voice-family', 332 'volume', 333 'white-space', 334 'width', 335 } 336 337 # survey of common keywords found in feeds 338 acceptable_css_keywords = { 339 '!important', 340 'aqua', 341 'auto', 342 'black', 343 'block', 344 'blue', 345 'bold', 346 'both', 347 'bottom', 348 'brown', 349 'center', 350 'collapse', 351 'dashed', 352 'dotted', 353 'fuchsia', 354 'gray', 355 'green', 356 'italic', 357 'left', 358 'lime', 359 'maroon', 360 'medium', 361 'navy', 362 'none', 363 'normal', 364 'nowrap', 365 'olive', 366 'pointer', 367 'purple', 368 'red', 369 'right', 370 'silver', 371 'solid', 372 'teal', 373 'top', 374 'transparent', 375 'underline', 376 'white', 377 'yellow', 378 } 379 380 valid_css_values = re.compile( 381 r'^(' 382 r'#[0-9a-f]+' # Hex values 383 r'|rgb\(\d+%?,\d*%?,?\d*%?\)?' # RGB values 384 r'|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?' # Sizes/widths 385 r')$' 386 ) 387 388 mathml_elements = { 389 'annotation', 390 'annotation-xml', 391 'maction', 392 'maligngroup', 393 'malignmark', 394 'math', 395 'menclose', 396 'merror', 397 'mfenced', 398 'mfrac', 399 'mglyph', 400 'mi', 401 'mlabeledtr', 402 'mlongdiv', 403 'mmultiscripts', 404 'mn', 405 'mo', 406 'mover', 407 'mpadded', 408 'mphantom', 409 'mprescripts', 410 'mroot', 411 'mrow', 412 'ms', 413 'mscarries', 414 'mscarry', 415 'msgroup', 416 'msline', 417 'mspace', 418 'msqrt', 419 'msrow', 420 'mstack', 421 'mstyle', 422 'msub', 423 'msubsup', 424 'msup', 425 'mtable', 426 'mtd', 427 'mtext', 428 'mtr', 429 'munder', 430 'munderover', 431 'none', 432 'semantics', 433 } 434 435 mathml_attributes = { 436 'accent', 437 'accentunder', 438 'actiontype', 439 'align', 440 'alignmentscope', 441 'altimg', 442 'altimg-height', 443 'altimg-valign', 444 'altimg-width', 445 'alttext', 446 'bevelled', 447 'charalign', 448 'close', 449 'columnalign', 450 'columnlines', 451 'columnspacing', 452 'columnspan', 453 'columnwidth', 454 'crossout', 455 'decimalpoint', 456 'denomalign', 457 'depth', 458 'dir', 459 'display', 460 'displaystyle', 461 'edge', 462 'encoding', 463 'equalcolumns', 464 'equalrows', 465 'fence', 466 'fontstyle', 467 'fontweight', 468 'form', 469 'frame', 470 'framespacing', 471 'groupalign', 472 'height', 473 'href', 474 'id', 475 'indentalign', 476 'indentalignfirst', 477 'indentalignlast', 478 'indentshift', 479 'indentshiftfirst', 480 'indentshiftlast', 481 'indenttarget', 482 'infixlinebreakstyle', 483 'largeop', 484 'length', 485 'linebreak', 486 'linebreakmultchar', 487 'linebreakstyle', 488 'lineleading', 489 'linethickness', 490 'location', 491 'longdivstyle', 492 'lquote', 493 'lspace', 494 'mathbackground', 495 'mathcolor', 496 'mathsize', 497 'mathvariant', 498 'maxsize', 499 'minlabelspacing', 500 'minsize', 501 'movablelimits', 502 'notation', 503 'numalign', 504 'open', 505 'other', 506 'overflow', 507 'position', 508 'rowalign', 509 'rowlines', 510 'rowspacing', 511 'rowspan', 512 'rquote', 513 'rspace', 514 'scriptlevel', 515 'scriptminsize', 516 'scriptsizemultiplier', 517 'selection', 518 'separator', 519 'separators', 520 'shift', 521 'side', 522 'src', 523 'stackalign', 524 'stretchy', 525 'subscriptshift', 526 'superscriptshift', 527 'symmetric', 528 'voffset', 529 'width', 530 'xlink:href', 531 'xlink:show', 532 'xlink:type', 533 'xmlns', 534 'xmlns:xlink', 535 } 536 537 # svgtiny - foreignObject + linearGradient + radialGradient + stop 538 svg_elements = { 539 'a', 540 'animate', 541 'animateColor', 542 'animateMotion', 543 'animateTransform', 544 'circle', 545 'defs', 546 'desc', 547 'ellipse', 548 'font-face', 549 'font-face-name', 550 'font-face-src', 551 'foreignObject', 552 'g', 553 'glyph', 554 'hkern', 555 'line', 556 'linearGradient', 557 'marker', 558 'metadata', 559 'missing-glyph', 560 'mpath', 561 'path', 562 'polygon', 563 'polyline', 564 'radialGradient', 565 'rect', 566 'set', 567 'stop', 568 'svg', 569 'switch', 570 'text', 571 'title', 572 'tspan', 573 'use', 574 } 575 576 # svgtiny + class + opacity + offset + xmlns + xmlns:xlink 577 svg_attributes = { 578 'accent-height', 579 'accumulate', 580 'additive', 581 'alphabetic', 582 'arabic-form', 583 'ascent', 584 'attributeName', 585 'attributeType', 586 'baseProfile', 587 'bbox', 588 'begin', 589 'by', 590 'calcMode', 591 'cap-height', 592 'class', 593 'color', 594 'color-rendering', 595 'content', 596 'cx', 597 'cy', 598 'd', 599 'descent', 600 'display', 601 'dur', 602 'dx', 603 'dy', 604 'end', 605 'fill', 606 'fill-opacity', 607 'fill-rule', 608 'font-family', 609 'font-size', 610 'font-stretch', 611 'font-style', 612 'font-variant', 613 'font-weight', 614 'from', 615 'fx', 616 'fy', 617 'g1', 618 'g2', 619 'glyph-name', 620 'gradientUnits', 621 'hanging', 622 'height', 623 'horiz-adv-x', 624 'horiz-origin-x', 625 'id', 626 'ideographic', 627 'k', 628 'keyPoints', 629 'keySplines', 630 'keyTimes', 631 'lang', 632 'marker-end', 633 'marker-mid', 634 'marker-start', 635 'markerHeight', 636 'markerUnits', 637 'markerWidth', 638 'mathematical', 639 'max', 640 'min', 641 'name', 642 'offset', 643 'opacity', 644 'orient', 645 'origin', 646 'overline-position', 647 'overline-thickness', 648 'panose-1', 649 'path', 650 'pathLength', 651 'points', 652 'preserveAspectRatio', 653 'r', 654 'refX', 655 'refY', 656 'repeatCount', 657 'repeatDur', 658 'requiredExtensions', 659 'requiredFeatures', 660 'restart', 661 'rotate', 662 'rx', 663 'ry', 664 'slope', 665 'stemh', 666 'stemv', 667 'stop-color', 668 'stop-opacity', 669 'strikethrough-position', 670 'strikethrough-thickness', 671 'stroke', 672 'stroke-dasharray', 673 'stroke-dashoffset', 674 'stroke-linecap', 675 'stroke-linejoin', 676 'stroke-miterlimit', 677 'stroke-opacity', 678 'stroke-width', 679 'systemLanguage', 680 'target', 681 'text-anchor', 682 'to', 683 'transform', 684 'type', 685 'u1', 686 'u2', 687 'underline-position', 688 'underline-thickness', 689 'unicode', 690 'unicode-range', 691 'units-per-em', 692 'values', 693 'version', 694 'viewBox', 695 'visibility', 696 'width', 697 'widths', 698 'x', 699 'x-height', 700 'x1', 701 'x2', 702 'xlink:actuate', 703 'xlink:arcrole', 704 'xlink:href', 705 'xlink:role', 706 'xlink:show', 707 'xlink:title', 708 'xlink:type', 709 'xml:base', 710 'xml:lang', 711 'xml:space', 712 'xmlns', 713 'xmlns:xlink', 714 'y', 715 'y1', 716 'y2', 717 'zoomAndPan', 718 } 719 720 svg_attr_map = None 721 svg_elem_map = None 722 723 acceptable_svg_properties = { 724 'fill', 725 'fill-opacity', 726 'fill-rule', 727 'stroke', 728 'stroke-linecap', 729 'stroke-linejoin', 730 'stroke-opacity', 731 'stroke-width', 732 } 733 734 def __init__(self, encoding=None, _type='application/xhtml+xml'): 735 super(_HTMLSanitizer, self).__init__(encoding, _type) 736 737 self.unacceptablestack = 0 738 self.mathmlOK = 0 739 self.svgOK = 0 740 741 def reset(self): 742 super(_HTMLSanitizer, self).reset() 743 self.unacceptablestack = 0 744 self.mathmlOK = 0 745 self.svgOK = 0 746 747 def unknown_starttag(self, tag, attrs): 748 acceptable_attributes = self.acceptable_attributes 749 keymap = {} 750 if tag not in self.acceptable_elements or self.svgOK: 751 if tag in self.unacceptable_elements_with_end_tag: 752 self.unacceptablestack += 1 753 754 # add implicit namespaces to html5 inline svg/mathml 755 if self._type.endswith('html'): 756 if not dict(attrs).get('xmlns'): 757 if tag == 'svg': 758 attrs.append(('xmlns', 'http://www.w3.org/2000/svg')) 759 if tag == 'math': 760 attrs.append(('xmlns', 'http://www.w3.org/1998/Math/MathML')) 761 762 # not otherwise acceptable, perhaps it is MathML or SVG? 763 if tag == 'math' and ('xmlns', 'http://www.w3.org/1998/Math/MathML') in attrs: 764 self.mathmlOK += 1 765 if tag == 'svg' and ('xmlns', 'http://www.w3.org/2000/svg') in attrs: 766 self.svgOK += 1 767 768 # chose acceptable attributes based on tag class, else bail 769 if self.mathmlOK and tag in self.mathml_elements: 770 acceptable_attributes = self.mathml_attributes 771 elif self.svgOK and tag in self.svg_elements: 772 # For most vocabularies, lowercasing is a good idea. Many 773 # svg elements, however, are camel case. 774 if not self.svg_attr_map: 775 lower = [attr.lower() for attr in self.svg_attributes] 776 mix = [a for a in self.svg_attributes if a not in lower] 777 self.svg_attributes = lower 778 self.svg_attr_map = {a.lower(): a for a in mix} 779 780 lower = [attr.lower() for attr in self.svg_elements] 781 mix = [a for a in self.svg_elements if a not in lower] 782 self.svg_elements = lower 783 self.svg_elem_map = {a.lower(): a for a in mix} 784 acceptable_attributes = self.svg_attributes 785 tag = self.svg_elem_map.get(tag, tag) 786 keymap = self.svg_attr_map 787 elif tag not in self.acceptable_elements: 788 return 789 790 # declare xlink namespace, if needed 791 if self.mathmlOK or self.svgOK: 792 if any((a for a in attrs if a[0].startswith('xlink:'))): 793 if not ('xmlns:xlink', 'http://www.w3.org/1999/xlink') in attrs: 794 attrs.append(('xmlns:xlink', 'http://www.w3.org/1999/xlink')) 795 796 clean_attrs = [] 797 for key, value in self.normalize_attrs(attrs): 798 if key == 'style' and 'style' in acceptable_attributes: 799 clean_value = self.sanitize_style(value) 800 if clean_value: 801 clean_attrs.append((key, clean_value)) 802 elif key in acceptable_attributes: 803 key = keymap.get(key, key) 804 # make sure the uri uses an acceptable uri scheme 805 if key == 'href': 806 value = make_safe_absolute_uri(value) 807 clean_attrs.append((key, value)) 808 super(_HTMLSanitizer, self).unknown_starttag(tag, clean_attrs) 809 810 def unknown_endtag(self, tag): 811 if tag not in self.acceptable_elements: 812 if tag in self.unacceptable_elements_with_end_tag: 813 self.unacceptablestack -= 1 814 if self.mathmlOK and tag in self.mathml_elements: 815 if tag == 'math' and self.mathmlOK: 816 self.mathmlOK -= 1 817 elif self.svgOK and tag in self.svg_elements: 818 tag = self.svg_elem_map.get(tag, tag) 819 if tag == 'svg' and self.svgOK: 820 self.svgOK -= 1 821 else: 822 return 823 super(_HTMLSanitizer, self).unknown_endtag(tag) 824 825 def handle_pi(self, text): 826 pass 827 828 def handle_decl(self, text): 829 pass 830 831 def handle_data(self, text): 832 if not self.unacceptablestack: 833 super(_HTMLSanitizer, self).handle_data(text) 834 835 def sanitize_style(self, style): 836 # disallow urls 837 style = re.compile(r'url\s*\(\s*[^\s)]+?\s*\)\s*').sub(' ', style) 838 839 # gauntlet 840 if not re.match(r"""^([:,;#%.\sa-zA-Z0-9!]|\w-\w|'[\s\w]+'|"[\s\w]+"|\([\d,\s]+\))*$""", style): 841 return '' 842 # This replaced a regexp that used re.match and was prone to 843 # pathological back-tracking. 844 if re.sub(r"\s*[-\w]+\s*:\s*[^:;]*;?", '', style).strip(): 845 return '' 846 847 clean = [] 848 for prop, value in re.findall(r"([-\w]+)\s*:\s*([^:;]*)", style): 849 if not value: 850 continue 851 if prop.lower() in self.acceptable_css_properties: 852 clean.append(prop + ': ' + value + ';') 853 elif prop.split('-')[0].lower() in ['background', 'border', 'margin', 'padding']: 854 for keyword in value.split(): 855 if ( 856 keyword not in self.acceptable_css_keywords 857 and not self.valid_css_values.match(keyword) 858 ): 859 break 860 else: 861 clean.append(prop + ': ' + value + ';') 862 elif self.svgOK and prop.lower() in self.acceptable_svg_properties: 863 clean.append(prop + ': ' + value + ';') 864 865 return ' '.join(clean) 866 867 def parse_comment(self, i, report=1): 868 ret = super(_HTMLSanitizer, self).parse_comment(i, report) 869 if ret >= 0: 870 return ret 871 # if ret == -1, this may be a malicious attempt to circumvent 872 # sanitization, or a page-destroying unclosed comment 873 match = re.compile(r'--[^>]*>').search(self.rawdata, i+4) 874 if match: 875 return match.end() 876 # unclosed comment; deliberately fail to handle_data() 877 return len(self.rawdata) 878 879 880 def _sanitize_html(html_source, encoding, _type): 881 p = _HTMLSanitizer(encoding, _type) 882 html_source = html_source.replace('<![CDATA[', '<![CDATA[') 883 p.feed(html_source) 884 data = p.output() 885 data = data.strip().replace('\r\n', '\n') 886 return data 887 888 889 # Match XML entity declarations. 890 # Example: <!ENTITY copyright "(C)"> 891 RE_ENTITY_PATTERN = re.compile(br'^\s*<!ENTITY([^>]*?)>', re.MULTILINE) 892 893 # Match XML DOCTYPE declarations. 894 # Example: <!DOCTYPE feed [ ]> 895 RE_DOCTYPE_PATTERN = re.compile(br'^\s*<!DOCTYPE([^>]*?)>', re.MULTILINE) 896 897 # Match safe entity declarations. 898 # This will allow hexadecimal character references through, 899 # as well as text, but not arbitrary nested entities. 900 # Example: cubed "³" 901 # Example: copyright "(C)" 902 # Forbidden: explode1 "&explode2;&explode2;" 903 RE_SAFE_ENTITY_PATTERN = re.compile(br'\s+(\w+)\s+"(&#\w+;|[^&"]*)"') 904 905 906 def replace_doctype(data): 907 """Strips and replaces the DOCTYPE, returns (rss_version, stripped_data) 908 909 rss_version may be 'rss091n' or None 910 stripped_data is the same XML document with a replaced DOCTYPE 911 """ 912 913 # Divide the document into two groups by finding the location 914 # of the first element that doesn't begin with '<?' or '<!'. 915 start = re.search(br'<\w', data) 916 start = start and start.start() or -1 917 head, data = data[:start+1], data[start+1:] 918 919 # Save and then remove all of the ENTITY declarations. 920 entity_results = RE_ENTITY_PATTERN.findall(head) 921 head = RE_ENTITY_PATTERN.sub(b'', head) 922 923 # Find the DOCTYPE declaration and check the feed type. 924 doctype_results = RE_DOCTYPE_PATTERN.findall(head) 925 doctype = doctype_results and doctype_results[0] or b'' 926 if b'netscape' in doctype.lower(): 927 version = 'rss091n' 928 else: 929 version = None 930 931 # Re-insert the safe ENTITY declarations if a DOCTYPE was found. 932 replacement = b'' 933 if len(doctype_results) == 1 and entity_results: 934 safe_entities = [ 935 e 936 for e in entity_results 937 if RE_SAFE_ENTITY_PATTERN.match(e) 938 ] 939 if safe_entities: 940 replacement = b'<!DOCTYPE feed [\n<!ENTITY' \ 941 + b'>\n<!ENTITY '.join(safe_entities) \ 942 + b'>\n]>' 943 data = RE_DOCTYPE_PATTERN.sub(replacement, head) + data 944 945 # Precompute the safe entities for the loose parser. 946 safe_entities = { 947 k.decode('utf-8'): v.decode('utf-8') 948 for k, v in RE_SAFE_ENTITY_PATTERN.findall(replacement) 949 } 950 return version, data, safe_entities 951