1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package org.htmlunit.javascript.host.xml;
16
17 import static org.htmlunit.BrowserVersionFeatures.JS_XSLT_TRANSFORM_INDENT;
18
19 import java.io.ByteArrayOutputStream;
20 import java.nio.charset.Charset;
21 import java.nio.charset.StandardCharsets;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.Map;
25
26 import javax.xml.XMLConstants;
27 import javax.xml.parsers.DocumentBuilderFactory;
28 import javax.xml.transform.OutputKeys;
29 import javax.xml.transform.Source;
30 import javax.xml.transform.Transformer;
31 import javax.xml.transform.TransformerFactory;
32 import javax.xml.transform.dom.DOMResult;
33 import javax.xml.transform.dom.DOMSource;
34 import javax.xml.transform.stream.StreamResult;
35
36 import org.htmlunit.SgmlPage;
37 import org.htmlunit.WebResponse;
38 import org.htmlunit.WebResponseData;
39 import org.htmlunit.html.DomDocumentFragment;
40 import org.htmlunit.html.DomNode;
41 import org.htmlunit.html.DomText;
42 import org.htmlunit.http.HttpStatus;
43 import org.htmlunit.javascript.HtmlUnitScriptable;
44 import org.htmlunit.javascript.JavaScriptEngine;
45 import org.htmlunit.javascript.configuration.JsxClass;
46 import org.htmlunit.javascript.configuration.JsxConstructor;
47 import org.htmlunit.javascript.configuration.JsxFunction;
48 import org.htmlunit.javascript.host.dom.Document;
49 import org.htmlunit.javascript.host.dom.DocumentFragment;
50 import org.htmlunit.javascript.host.dom.Node;
51 import org.htmlunit.util.EncodingSniffer;
52 import org.htmlunit.util.XmlUtils;
53 import org.htmlunit.xml.XmlPage;
54 import org.w3c.dom.NodeList;
55
56
57
58
59
60
61
62
63
64
65 @JsxClass
66 public class XSLTProcessor extends HtmlUnitScriptable {
67
68 private Node style_;
69 private final Map<String, Object> parameters_ = new HashMap<>();
70
71
72
73
74 @JsxConstructor
75 public void jsConstructor() {
76
77 }
78
79
80
81
82
83
84
85
86
87 @JsxFunction
88 public void importStylesheet(final Node style) {
89 style_ = style;
90 }
91
92
93
94
95
96
97
98
99 @JsxFunction
100 public XMLDocument transformToDocument(final Node source) {
101 final XMLDocument doc = new XMLDocument();
102 doc.setPrototype(getPrototype(doc.getClass()));
103 doc.setParentScope(getParentScope());
104
105 final Object transformResult = transform(source);
106 final org.w3c.dom.Node node;
107 if (transformResult instanceof org.w3c.dom.Node transformedDoc) {
108 node = transformedDoc.getFirstChild();
109 }
110 else {
111 node = null;
112 }
113 final XmlPage page = new XmlPage(node, getWindow().getWebWindow());
114 doc.setDomNode(page);
115 return doc;
116 }
117
118
119
120
121
122
123
124 private Object transform(final Node source) {
125 try {
126 final DomNode sourceDomNode = source.getDomNodeOrDie();
127 Source xmlSource = new DOMSource(sourceDomNode);
128
129 final DomNode xsltDomNode = style_.getDomNodeOrDie();
130 final Source xsltSource = new DOMSource(xsltDomNode);
131
132 final TransformerFactory transformerFactory = TransformerFactory.newInstance();
133
134
135
136
137 transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
138 try {
139 transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
140 }
141 catch (final IllegalArgumentException ignored) {
142
143 }
144 try {
145 transformerFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
146 }
147 catch (final IllegalArgumentException ignored) {
148
149 }
150
151 final SgmlPage page = sourceDomNode.getPage();
152 if (page != null && page.getWebClient().getBrowserVersion()
153 .hasFeature(JS_XSLT_TRANSFORM_INDENT)) {
154 final DomNode outputNode = findOutputNode(xsltDomNode);
155 if (outputNode != null) {
156 final org.w3c.dom.Node indentNode = outputNode.getAttributes().getNamedItem("indent");
157 if (indentNode != null && "yes".equalsIgnoreCase(indentNode.getNodeValue())) {
158 try {
159 transformerFactory.setAttribute("indent-number", Integer.valueOf(2));
160 }
161 catch (final IllegalArgumentException ignored) {
162
163 }
164 final Transformer transformer = transformerFactory.newTransformer(xsltSource);
165 transformer.setOutputProperty(OutputKeys.INDENT, "yes");
166 try {
167 transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
168 }
169 catch (final IllegalArgumentException ignored) {
170
171 }
172
173 for (final Map.Entry<String, Object> entry : parameters_.entrySet()) {
174 transformer.setParameter(entry.getKey(), entry.getValue());
175 }
176
177
178
179
180 try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
181 transformer.transform(xmlSource, new StreamResult(out));
182 final WebResponseData data = new WebResponseData(out.toByteArray(),
183 HttpStatus.OK_200, HttpStatus.OK_200_MSG, Collections.emptyList());
184 final WebResponse response = new WebResponse(data, null, 0) {
185
186
187
188 @Override
189 public Charset getContentCharset() {
190 final Charset cs = EncodingSniffer.toCharset(
191 transformer.getOutputProperty(OutputKeys.ENCODING));
192 if (cs == null) {
193 return StandardCharsets.UTF_8;
194 }
195 return cs;
196 }
197 };
198 return XmlUtils.buildDocument(response);
199 }
200 }
201 }
202 }
203
204 final Transformer transformer = transformerFactory.newTransformer(xsltSource);
205 transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
206
207 for (final Map.Entry<String, Object> entry : parameters_.entrySet()) {
208 transformer.setParameter(entry.getKey(), entry.getValue());
209 }
210
211 final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
212 try {
213 factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
214 }
215 catch (final javax.xml.parsers.ParserConfigurationException ignored) {
216
217 }
218 final org.w3c.dom.Document containerDocument = factory.newDocumentBuilder().newDocument();
219 final org.w3c.dom.Element containerElement = containerDocument.createElement("container");
220 containerDocument.appendChild(containerElement);
221
222 final DOMResult result = new DOMResult(containerElement);
223 transformer.transform(xmlSource, result);
224
225 final org.w3c.dom.Node transformedNode = result.getNode();
226 final org.w3c.dom.Node transformedFirstChild = transformedNode.getFirstChild();
227 if (transformedFirstChild != null && transformedFirstChild.getNodeType() == Node.ELEMENT_NODE) {
228 return transformedNode;
229 }
230
231
232 xmlSource = new DOMSource(source.getDomNodeOrDie());
233 try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
234 transformer.transform(xmlSource, new StreamResult(out));
235
236 final Charset cs = EncodingSniffer.toCharset(transformer.getOutputProperty(OutputKeys.ENCODING));
237 if (cs == null) {
238 return new String(out.toByteArray(), StandardCharsets.UTF_8);
239 }
240 return new String(out.toByteArray(), cs);
241 }
242 }
243 catch (final RuntimeException e) {
244 throw e;
245 }
246 catch (final Exception e) {
247 throw JavaScriptEngine.reportRuntimeError("Exception: " + e);
248 }
249 }
250
251
252
253
254
255
256
257
258
259 @JsxFunction
260 public DocumentFragment transformToFragment(final Node source, final Object output) {
261 final SgmlPage page = (SgmlPage) ((Document) output).getDomNodeOrDie();
262
263 final DomDocumentFragment fragment = page.createDocumentFragment();
264 final DocumentFragment rv = new DocumentFragment();
265 rv.setPrototype(getPrototype(rv.getClass()));
266 rv.setParentScope(getParentScope());
267 rv.setDomNode(fragment);
268
269 final Object result = transform(source);
270 if (result instanceof org.w3c.dom.Node node) {
271 final SgmlPage parentPage = fragment.getPage();
272 final NodeList children = node.getChildNodes();
273 final int length = children.getLength();
274 for (int i = 0; i < length; i++) {
275 XmlUtils.appendChild(parentPage, fragment, children.item(i), true);
276 }
277 }
278 else {
279 final DomText text = new DomText(fragment.getPage(), (String) result);
280 fragment.appendChild(text);
281 }
282
283 return rv;
284 }
285
286
287
288
289
290
291
292
293
294 @JsxFunction
295 public void setParameter(final String namespaceURI, final String localName, final Object value) {
296 parameters_.put(getQualifiedName(namespaceURI, localName), value);
297 }
298
299
300
301
302
303
304
305
306 @JsxFunction
307 public Object getParameter(final String namespaceURI, final String localName) {
308 return parameters_.get(getQualifiedName(namespaceURI, localName));
309 }
310
311 private static String getQualifiedName(final String namespaceURI, final String localName) {
312 final String qualifiedName;
313 if (namespaceURI != null && !namespaceURI.isEmpty() && !"null".equals(namespaceURI)) {
314 qualifiedName = '{' + namespaceURI + '}' + localName;
315 }
316 else {
317 qualifiedName = localName;
318 }
319 return qualifiedName;
320 }
321
322 private static DomNode findOutputNode(final DomNode xsltDomNode) {
323 for (final DomNode child : xsltDomNode.getChildren()) {
324 if ("output".equals(child.getLocalName())) {
325 return child;
326 }
327
328 for (final DomNode child1 : child.getChildren()) {
329 if ("output".equals(child1.getLocalName())) {
330 return child1;
331 }
332 }
333 }
334 return null;
335 }
336 }