116 lines
4.1 KiB
Python
116 lines
4.1 KiB
Python
"""
|
|
Maakt HTML uit de handleiding-editor veilig: enkel een vaste lijst tags en attributen blijft over.
|
|
Scripts, event-handlers (onclick, ...), stijlen en javascript:-links verdwijnen.
|
|
"""
|
|
from html import escape
|
|
from html.parser import HTMLParser
|
|
|
|
TOEGELATEN = {
|
|
'p': (), 'br': (), 'hr': (), 'div': (), 'span': (),
|
|
'h2': ('id',), 'h3': ('id',), 'h4': ('id',),
|
|
'strong': (), 'b': (), 'em': (), 'i': (), 'u': (), 's': (), 'mark': (), 'kbd': (), 'sub': (), 'sup': (),
|
|
'ul': (), 'ol': ('start',), 'li': (),
|
|
'blockquote': (), 'code': (), 'pre': (),
|
|
'a': ('href', 'title', 'target'),
|
|
'img': ('src', 'alt', 'title', 'width'),
|
|
'table': (), 'thead': (), 'tbody': (), 'tr': (), 'th': ('colspan', 'rowspan'), 'td': ('colspan', 'rowspan'),
|
|
'figure': (), 'figcaption': (),
|
|
}
|
|
LEEG = {'br', 'hr', 'img'}
|
|
KLASSEN = {'tip', 'let-op', 'info'} # kaders in de editor
|
|
WEGGOOIEN_MET_INHOUD = {'script', 'style', 'iframe', 'object', 'embed', 'noscript', 'template', 'svg', 'math'}
|
|
# Word/Docs plakt deze tags; de inhoud blijft, de tag niet
|
|
VERTALING = {'b': 'strong', 'i': 'em', 'h1': 'h2', 'h5': 'h4', 'h6': 'h4'}
|
|
|
|
|
|
def _veilige_url(url, afbeelding=False):
|
|
u = (url or '').strip()
|
|
laag = u.lower().replace('\x00', '')
|
|
if afbeelding and laag.startswith('data:image/') and ';base64,' in laag and not laag.startswith('data:image/svg'):
|
|
return u
|
|
if laag.startswith(('http://', 'https://', '/', '#')) or (not afbeelding and laag.startswith('mailto:')):
|
|
return u
|
|
return None
|
|
|
|
|
|
class _Schoonmaker(HTMLParser):
|
|
def __init__(self):
|
|
super().__init__(convert_charrefs=True)
|
|
self.uit = []
|
|
self.open = []
|
|
self.weg = 0
|
|
|
|
def handle_starttag(self, tag, attrs):
|
|
if tag in WEGGOOIEN_MET_INHOUD:
|
|
if tag not in LEEG:
|
|
self.weg += 1
|
|
return
|
|
if self.weg:
|
|
return
|
|
tag = VERTALING.get(tag, tag)
|
|
if tag not in TOEGELATEN:
|
|
return
|
|
delen = [tag]
|
|
for naam, waarde in attrs:
|
|
naam = (naam or '').lower()
|
|
if naam == 'class':
|
|
klassen = [k for k in (waarde or '').split() if k in KLASSEN]
|
|
if klassen:
|
|
delen.append(f'class="{escape(" ".join(klassen))}"')
|
|
continue
|
|
if naam not in TOEGELATEN[tag]:
|
|
continue
|
|
if naam in ('href', 'src'):
|
|
waarde = _veilige_url(waarde, afbeelding=(naam == 'src'))
|
|
if waarde is None:
|
|
continue
|
|
if naam == 'target':
|
|
waarde = '_blank'
|
|
if naam in ('width', 'colspan', 'rowspan', 'start') and not str(waarde or '').isdigit():
|
|
continue
|
|
delen.append(f'{naam}="{escape(waarde or "", quote=True)}"')
|
|
if tag == 'a' and any(d.startswith('target=') for d in delen):
|
|
delen.append('rel="noopener noreferrer"')
|
|
if tag == 'img' and not any(d.startswith('src=') for d in delen):
|
|
return
|
|
self.uit.append('<' + ' '.join(delen) + '>')
|
|
if tag not in LEEG:
|
|
self.open.append(tag)
|
|
|
|
def handle_startendtag(self, tag, attrs):
|
|
self.handle_starttag(tag, attrs)
|
|
tag = VERTALING.get(tag, tag)
|
|
if tag not in LEEG and self.open and self.open[-1] == tag:
|
|
self.handle_endtag(tag)
|
|
|
|
def handle_endtag(self, tag):
|
|
if tag in WEGGOOIEN_MET_INHOUD:
|
|
self.weg = max(0, self.weg - 1)
|
|
return
|
|
if self.weg:
|
|
return
|
|
tag = VERTALING.get(tag, tag)
|
|
if tag not in TOEGELATEN or tag in LEEG or tag not in self.open:
|
|
return
|
|
while self.open:
|
|
t = self.open.pop()
|
|
self.uit.append(f'</{t}>')
|
|
if t == tag:
|
|
break
|
|
|
|
def handle_data(self, data):
|
|
if not self.weg:
|
|
self.uit.append(escape(data, quote=False))
|
|
|
|
def resultaat(self):
|
|
while self.open:
|
|
self.uit.append(f'</{self.open.pop()}>')
|
|
return ''.join(self.uit)
|
|
|
|
|
|
def schoon(html):
|
|
p = _Schoonmaker()
|
|
p.feed(html or '')
|
|
p.close()
|
|
return p.resultaat().strip()
|