1#************************************************************** 2# 3# Licensed to the Apache Software Foundation (ASF) under one 4# or more contributor license agreements. See the NOTICE file 5# distributed with this work for additional information 6# regarding copyright ownership. The ASF licenses this file 7# to you under the Apache License, Version 2.0 (the 8# "License"); you may not use this file except in compliance 9# with the License. You may obtain a copy of the License at 10# 11# http://www.apache.org/licenses/LICENSE-2.0 12# 13# Unless required by applicable law or agreed to in writing, 14# software distributed under the License is distributed on an 15# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16# KIND, either express or implied. See the License for the 17# specific language governing permissions and limitations 18# under the License. 19# 20#************************************************************** 21 22# Removes foreign elements and attributes (ODF 1.2 Part 1 3.17) so that the rest can be validated as an 23# extended document (2.2.2 B.1). Loaded by validate.py and selftest.py. 24# foreign attribute: namespace outside tables 1-4 of 1.5 (xml: included), removed. 25# foreign element: namespace outside tables 1-4, outside any element of tables 1-4 (their wildcard 26# content is the schema's), removed with its content, except that its tags alone are removed when 27# it sits in a text:p or text:h and every ancestor up to it permits character data in the schema. 28# office:process-content (deprecated in 1.2) is not consulted. Table 4 names that the schema does not 29# define are not removed, so they stay visible as errors. 30import re 31from xml.etree import ElementTree as ET 32from xml.parsers import expat 33from xml.sax.saxutils import escape, quoteattr 34 35ODF = "urn:oasis:names:tc:opendocument:xmlns:%s:1.0" 36T123 = {ODF % x for x in "animation chart config database dr3d drawing form manifest meta datastyle office presentation script table text style " 37 "xsl-fo-compatible svg-compatible smil-compatible".split()} | {"http://docs.oasis-open.org/ns/office/1.2/meta/odf#"} 38T4 = {"http://purl.org/dc/elements/1.1/", "http://www.w3.org/1998/Math/MathML", "http://www.w3.org/2002/xforms", "http://www.w3.org/1999/xlink", 39 "http://www.w3.org/1999/xhtml", "http://www.w3.org/2003/g/data-view#", "http://www.w3.org/XML/1998/namespace"} 40RNG = "{http://relaxng.org/ns/structure/1.0}" 41TEXT_P, TEXT_H = "{%s}p" % (ODF % "text"), "{%s}h" % (ODF % "text") 42 43 44def mixed_elements(rng_path): 45 """Expanded names of the schema's elements whose content model permits character data.""" 46 prefixes = dict(re.findall(r'xmlns:(\w+)="([^"]+)"', open(rng_path, encoding="utf-8").read(4000))) 47 defines = {} 48 for d in ET.parse(rng_path).getroot().iter(RNG + "define"): 49 defines.setdefault(d.get("name"), []).append(d) 50 memo = {} 51 52 def has_text(node, seen): 53 tag = node.tag 54 if tag in (RNG + "text", RNG + "mixed"): 55 return True 56 if tag in (RNG + "element", RNG + "attribute", RNG + "data", RNG + "value"): 57 return False 58 if tag == RNG + "ref": 59 n = node.get("name") 60 if n in seen: 61 return False 62 return any(has_text(c, seen | {n}) for d in defines.get(n, []) for c in d) 63 return any(has_text(c, seen) for c in node) 64 out = set() 65 for el in ET.parse(rng_path).getroot().iter(RNG + "element"): 66 name = el.get("name") 67 if name and ":" in name: 68 pre, local = name.split(":", 1) 69 if any(has_text(c, frozenset()) for c in el): 70 out.add("{%s}%s" % (prefixes[pre], local)) 71 return out 72 73 74def strip_foreign(text, mixed): 75 out, scopes, frames, state = [], [{"xml": "http://www.w3.org/XML/1998/namespace"}], [], {"drop": 0, "pending": {}} 76 p = expat.ParserCreate() 77 p.ordered_attributes = True 78 79 def expand(name, scope, is_attr): 80 """Expanded name, or None when the prefix is unbound (kept, so that the validator reports it).""" 81 if ":" in name: 82 pre, local = name.split(":", 1) 83 return "{%s}%s" % (scope[pre], local) if pre in scope else None 84 uri = "" if is_attr else scope.get("", "") 85 return "{%s}%s" % (uri, name) if uri else name 86 87 def ns_of(expanded): 88 return expanded[1:].split("}", 1)[0] if expanded and expanded.startswith("{") else "" 89 90 def start(name, attrs): 91 if state["drop"]: 92 state["drop"] += 1 93 return 94 decl = {} 95 for i in range(0, len(attrs), 2): 96 if attrs[i] == "xmlns" or attrs[i].startswith("xmlns:"): 97 decl[attrs[i][6:]] = attrs[i + 1] 98 scope = dict(scopes[-1]) 99 scope.update(decl) 100 exp = expand(name, scope, False) 101 in_t4 = any(ns_of(f[0]) in T4 for f in frames if f[1]) 102 if exp is not None and ns_of(exp) not in T123 | T4 and not in_t4: 103 if unwrap(frames): 104 scopes.append(scope) 105 frames.append((exp, False)) 106 state["pending"].update(decl) 107 else: 108 state["drop"] = 1 109 return 110 kept = [] 111 for i in range(0, len(attrs), 2): 112 k, v = attrs[i], attrs[i + 1] 113 ke = expand(k, scope, True) 114 if k == "xmlns" or k.startswith("xmlns:") or ":" not in k or ke is None or ns_of(ke) in T123 | T4: 115 kept.append((k, v)) 116 have = {k for k, _ in kept} 117 for pre, uri in state["pending"].items(): 118 k = "xmlns:" + pre if pre else "xmlns" 119 if k not in have: 120 kept.insert(0, (k, uri)) 121 state["pending"] = {} 122 out.append("<" + name + "".join(" %s=%s" % (k, quoteattr(v, {"\n": " ", "\t": "	", "\r": " "})) for k, v in kept) + ">") 123 scopes.append(scope) 124 frames.append((exp, True)) 125 126 def end(name): 127 if state["drop"]: 128 state["drop"] -= 1 129 return 130 scopes.pop() 131 if frames.pop()[1]: 132 out.append("</" + name + ">") 133 134 def data(s): 135 if not state["drop"]: 136 out.append(escape(s)) 137 138 def unwrap(fr): 139 """Foreign element in a paragraph, every ancestor up to it permitting character data?""" 140 for exp, kept in reversed(fr): 141 if not kept: 142 continue 143 if exp in (TEXT_P, TEXT_H): 144 return True 145 if exp not in mixed: 146 return False 147 return False 148 p.StartElementHandler, p.EndElementHandler, p.CharacterDataHandler = start, end, data 149 p.Parse(text.encode("utf-8") if isinstance(text, str) else text, True) 150 return '<?xml version="1.0" encoding="UTF-8"?>\n' + "".join(out) + "\n" 151