Format: text/x-python
⬇ Download File (23.3 KB)
   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[', '&lt;![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 "&#179;"
 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