1 /*
2 * Copyright (c) 2002-2026 Gargoyle Software Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 * https://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15 package org.htmlunit.html;
16
17 import java.io.PrintWriter;
18 import java.io.StringWriter;
19 import java.util.Map;
20
21 import org.htmlunit.SgmlPage;
22 import org.htmlunit.javascript.AbstractJavaScriptEngine;
23 import org.htmlunit.javascript.PostponedAction;
24 import org.htmlunit.javascript.host.dom.Document;
25
26 /**
27 * Wrapper for the HTML element "script".<br>
28 * When a script tag references an external script (with attribute src) it gets executed when the node
29 * is added to the DOM tree. When the script code is nested, it gets executed when the text node
30 * containing the script is added to the HtmlScript.<br>
31 * The ScriptFilter feature of NekoHtml can't be used because it doesn't allow immediate access to the DOM
32 * (i.e. <code>document.write("<span id='mySpan'/>"); document.getElementById("mySpan").tagName;</code>
33 * can't work with a filter).
34 *
35 * @author Mike Bowler
36 * @author Christian Sell
37 * @author Marc Guillemot
38 * @author David K. Taylor
39 * @author Ahmed Ashour
40 * @author Daniel Gredler
41 * @author Dmitri Zoubkov
42 * @author Sudhan Moghe
43 * @author Ronald Brill
44 * @author Daniel Wagner-Hall
45 * @author Frank Danek
46 * @see <a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-one-html.html#ID-81598695">DOM Level 1</a>
47 * @see <a href="http://www.w3.org/TR/2003/REC-DOM-Level-2-HTML-20030109/html.html#ID-81598695">DOM Level 2</a>
48 */
49 public class HtmlScript extends HtmlElement implements ScriptElement {
50
51 /** The HTML tag represented by this element. */
52 public static final String TAG_NAME = "script";
53
54 private boolean executed_;
55 private boolean createdByDomParser_;
56
57 /**
58 * Creates an instance of HtmlScript.
59 *
60 * @param qualifiedName the qualified name of the element type to instantiate
61 * @param page the HtmlPage that contains this element
62 * @param attributes the initial attributes
63 */
64 HtmlScript(final String qualifiedName, final SgmlPage page,
65 final Map<String, DomAttr> attributes) {
66 super(qualifiedName, page, attributes);
67 }
68
69 /**
70 * Returns the value of the attribute {@code charset}. Refer to the
71 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
72 * documentation for details on the use of this attribute.
73 *
74 * @return the value of the attribute {@code charset}
75 * or an empty string if that attribute isn't defined.
76 */
77 public final String getCharsetAttribute() {
78 return getAttributeDirect("charset");
79 }
80
81 /**
82 * {@inheritDoc}
83 */
84 @Override
85 public final String getScriptCharset() {
86 return getAttributeDirect("charset");
87 }
88
89 /**
90 * Returns the value of the attribute {@code type}. Refer to the
91 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
92 * documentation for details on the use of this attribute.
93 *
94 * @return the value of the attribute {@code type}
95 * or an empty string if that attribute isn't defined.
96 */
97 public final String getTypeAttribute() {
98 return getAttributeDirect(TYPE_ATTRIBUTE);
99 }
100
101 /**
102 * Returns the value of the attribute {@code language}. Refer to the
103 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
104 * documentation for details on the use of this attribute.
105 *
106 * @return the value of the attribute {@code language}
107 * or an empty string if that attribute isn't defined.
108 */
109 public final String getLanguageAttribute() {
110 return getAttributeDirect("language");
111 }
112
113 /**
114 * Returns the value of the attribute {@code src}. Refer to the
115 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
116 * documentation for details on the use of this attribute.
117 *
118 * @return the value of the attribute {@code src}
119 * or an empty string if that attribute isn't defined.
120 */
121 public final String getSrcAttribute() {
122 return getSrcAttributeNormalized();
123 }
124
125 /**
126 * {@inheritDoc}
127 */
128 @Override
129 public final String getScriptSource() {
130 return getSrcAttributeNormalized();
131 }
132
133 /**
134 * Returns the value of the attribute {@code event}.
135 * @return the value of the attribute {@code event}
136 */
137 public final String getEventAttribute() {
138 return getAttributeDirect("event");
139 }
140
141 /**
142 * Returns the value of the attribute {@code for}.
143 * @return the value of the attribute {@code for}
144 */
145 public final String getHtmlForAttribute() {
146 return getAttributeDirect("for");
147 }
148
149 /**
150 * Returns the value of the attribute {@code defer}. Refer to the
151 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
152 * documentation for details on the use of this attribute.
153 *
154 * @return the value of the attribute {@code defer}
155 * or an empty string if that attribute isn't defined.
156 */
157 public final String getDeferAttribute() {
158 return getAttributeDirect("defer");
159 }
160
161 /**
162 * {@inheritDoc}
163 */
164 @Override
165 public boolean isDeferred() {
166 return getDeferAttribute() != ATTRIBUTE_NOT_DEFINED;
167 }
168
169 /**
170 * {@inheritDoc}
171 */
172 @Override
173 public boolean isCrossorigin() {
174 return hasAttribute("crossorigin");
175 }
176
177 /**
178 * {@inheritDoc}
179 */
180 @Override
181 public boolean mayBeDisplayed() {
182 return false;
183 }
184
185 /**
186 * If setting the <code>src</code> attribute, this method executes the new JavaScript if necessary
187 * (behavior varies by browser version). {@inheritDoc}
188 */
189 @Override
190 protected void setAttributeNS(final String namespaceURI, final String qualifiedName, final String attributeValue,
191 final boolean notifyAttributeChangeListeners, final boolean notifyMutationObservers) {
192 final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCase(qualifiedName);
193 // special additional processing for the 'src'
194 if (namespaceURI != null || !SRC_ATTRIBUTE.equals(qualifiedNameLC)) {
195 super.setAttributeNS(namespaceURI, qualifiedNameLC, attributeValue, notifyAttributeChangeListeners,
196 notifyMutationObservers);
197 return;
198 }
199
200 // namespaceURI is always null here - we can call getAttribute directly
201 final String oldValue = getAttribute(qualifiedNameLC);
202 super.setAttributeNS(null, qualifiedNameLC, attributeValue, notifyAttributeChangeListeners,
203 notifyMutationObservers);
204
205 if (isAttachedToPage() && oldValue.isEmpty() && getFirstChild() == null) {
206 final PostponedAction action = new PostponedAction(getPage(), "HtmlScript.setAttributeNS") {
207 @Override
208 public void execute() {
209 ScriptElementSupport.executeScriptIfNeeded(HtmlScript.this, false, false);
210 }
211 };
212 final AbstractJavaScriptEngine<?> engine = getPage().getWebClient().getJavaScriptEngine();
213 engine.addPostponedAction(action);
214 }
215 }
216
217 /**
218 * Executes the <code>onreadystatechange</code> handler, as well as executing
219 * the script itself, if necessary.
220 * {@inheritDoc}
221 */
222 @Override
223 public void onAllChildrenAddedToPage(final boolean postponed) {
224 ScriptElementSupport.onAllChildrenAddedToPage(this, postponed);
225 }
226
227 /**
228 * Gets the script held within the script tag.
229 */
230 private String getScriptCode() {
231 final Iterable<DomNode> textNodes = getChildren();
232 final StringBuilder scriptCode = new StringBuilder();
233 for (final DomNode node : textNodes) {
234 if (node instanceof DomText domText) {
235 scriptCode.append(domText.getData());
236 }
237 }
238 return scriptCode.toString();
239 }
240
241 /**
242 * Indicates if a node without children should be written in expanded form as XML
243 * (i.e. with closing tag rather than with "/>")
244 * @return {@code true} to make generated XML readable as HTML
245 */
246 @Override
247 protected boolean isEmptyXmlTagExpanded() {
248 return true;
249 }
250
251 /**
252 * {@inheritDoc}
253 */
254 @Override
255 protected boolean printChildrenAsXml(final String indent, final boolean tagBefore, final PrintWriter printWriter) {
256 final DomCharacterData textNode = (DomCharacterData) getFirstChild();
257 if (textNode == null) {
258 return tagBefore;
259 }
260
261 final String data = textNode.getData();
262 printWriter.print("\r\n");
263 if (data.contains("//<![CDATA[")) {
264 printWriter.print(data);
265 }
266 else {
267 printWriter.print("//<![CDATA[");
268 printWriter.print("\r\n");
269 printWriter.print(data);
270 printWriter.print("\r\n");
271 printWriter.print("//]]>");
272 }
273 return true;
274 }
275
276 /**
277 * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
278 *
279 * Resets the executed flag.
280 * @see HtmlScript#processImportNode(Document)
281 */
282 public void resetExecuted() {
283 executed_ = false;
284 }
285
286 @Override
287 public void processImportNode(final Document doc) {
288 super.processImportNode(doc);
289
290 executed_ = true;
291 }
292
293 /**
294 * Returns a string representation of this object.
295 * @return a string representation of this object
296 */
297 @Override
298 public String toString() {
299 final StringWriter writer = new StringWriter();
300 final PrintWriter printWriter = new PrintWriter(writer);
301
302 printWriter.print(getClass().getSimpleName());
303 printWriter.print("[<");
304 printOpeningTagContentAsXml(printWriter);
305 printWriter.print(">");
306 printWriter.print(getScriptCode());
307 printWriter.print("]");
308 printWriter.flush();
309 return writer.toString();
310 }
311
312 /**
313 * {@inheritDoc}
314 */
315 @Override
316 public DisplayStyle getDefaultStyleDisplay() {
317 return DisplayStyle.NONE;
318 }
319
320 /**
321 * {@inheritDoc}
322 */
323 @Override
324 public void markAsCreatedByDomParser() {
325 createdByDomParser_ = true;
326 }
327
328 /**
329 * {@inheritDoc}
330 */
331 @Override
332 public boolean wasCreatedByDomParser() {
333 return createdByDomParser_;
334 }
335
336 /**
337 * {@inheritDoc}
338 */
339 @Override
340 public boolean isExecuted() {
341 return executed_;
342 }
343
344 /**
345 * {@inheritDoc}
346 */
347 @Override
348 public void setExecuted(final boolean executed) {
349 executed_ = executed;
350 }
351 }