View Javadoc
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.javascript.host.html;
16  
17  import static org.htmlunit.BrowserVersionFeatures.HTMLDOCUMENT_ELEMENTS_BY_NAME_EMPTY;
18  import static org.htmlunit.javascript.configuration.SupportedBrowser.FF;
19  import static org.htmlunit.javascript.configuration.SupportedBrowser.FF_ESR;
20  
21  import java.io.IOException;
22  import java.io.Serializable;
23  import java.net.URL;
24  import java.util.ArrayList;
25  import java.util.List;
26  import java.util.function.Supplier;
27  
28  import org.apache.commons.lang3.StringUtils;
29  import org.apache.commons.logging.Log;
30  import org.apache.commons.logging.LogFactory;
31  import org.htmlunit.ScriptResult;
32  import org.htmlunit.StringWebResponse;
33  import org.htmlunit.WebClient;
34  import org.htmlunit.WebWindow;
35  import org.htmlunit.corejs.javascript.Context;
36  import org.htmlunit.corejs.javascript.Function;
37  import org.htmlunit.corejs.javascript.Scriptable;
38  import org.htmlunit.corejs.javascript.VarScope;
39  import org.htmlunit.html.BaseFrameElement;
40  import org.htmlunit.html.DomDocumentFragment;
41  import org.htmlunit.html.DomElement;
42  import org.htmlunit.html.DomNode;
43  import org.htmlunit.html.FrameWindow;
44  import org.htmlunit.html.HtmlAttributeChangeEvent;
45  import org.htmlunit.html.HtmlElement;
46  import org.htmlunit.html.HtmlForm;
47  import org.htmlunit.html.HtmlImage;
48  import org.htmlunit.html.HtmlPage;
49  import org.htmlunit.html.HtmlScript;
50  import org.htmlunit.javascript.HtmlUnitScriptable;
51  import org.htmlunit.javascript.JavaScriptEngine;
52  import org.htmlunit.javascript.PostponedAction;
53  import org.htmlunit.javascript.configuration.JsxClass;
54  import org.htmlunit.javascript.configuration.JsxConstructor;
55  import org.htmlunit.javascript.configuration.JsxFunction;
56  import org.htmlunit.javascript.configuration.JsxGetter;
57  import org.htmlunit.javascript.configuration.JsxStaticFunction;
58  import org.htmlunit.javascript.host.Element;
59  import org.htmlunit.javascript.host.dom.AbstractList.EffectOnCache;
60  import org.htmlunit.javascript.host.dom.Attr;
61  import org.htmlunit.javascript.host.dom.Document;
62  import org.htmlunit.javascript.host.dom.Node;
63  import org.htmlunit.javascript.host.dom.NodeList;
64  import org.htmlunit.javascript.host.dom.Selection;
65  import org.htmlunit.javascript.host.event.Event;
66  import org.htmlunit.util.UrlUtils;
67  
68  /**
69   * A JavaScript object for {@code HTMLDocument}.
70   *
71   * @author Mike Bowler
72   * @author David K. Taylor
73   * @author Chen Jun
74   * @author Christian Sell
75   * @author Chris Erskine
76   * @author Marc Guillemot
77   * @author Daniel Gredler
78   * @author Michael Ottati
79   * @author George Murnock
80   * @author Ahmed Ashour
81   * @author Rob Di Marco
82   * @author Sudhan Moghe
83   * @author Mike Dirolf
84   * @author Ronald Brill
85   * @author Frank Danek
86   * @author Sven Strickroth
87   *
88   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document">MDN Documentation</a>
89   * @see <a href="http://www.w3.org/TR/2000/WD-DOM-Level-1-20000929/level-one-html.html#ID-7068919">
90   *     W3C DOM Level 1</a>
91   */
92  @JsxClass
93  public class HTMLDocument extends Document {
94  
95      private static final Log LOG = LogFactory.getLog(HTMLDocument.class);
96  
97      private enum ParsingStatus { OUTSIDE, START, IN_NAME, INSIDE, IN_STRING }
98  
99      /** The buffer that will be used for calls to document.write(). */
100     private final StringBuilder writeBuilder_ = new StringBuilder();
101     private boolean writeInCurrentDocument_ = true;
102 
103     private boolean closePostponedAction_;
104     private boolean executionExternalPostponed_;
105 
106     /**
107      * JavaScript constructor.
108      */
109     @Override
110     @JsxConstructor
111     public void jsConstructor() {
112         super.jsConstructor();
113     }
114 
115     /**
116      * {@inheritDoc}
117      */
118     @Override
119     public DomNode getDomNodeOrDie() {
120         try {
121             return super.getDomNodeOrDie();
122         }
123         catch (final IllegalStateException e) {
124             throw JavaScriptEngine.typeError("No node attached to this object");
125         }
126     }
127 
128     /**
129      * Returns the HTML page that this document is modeling.
130      * @return the HTML page that this document is modeling
131      */
132     @Override
133     public HtmlPage getPage() {
134         return (HtmlPage) getDomNodeOrDie();
135     }
136 
137     /**
138      * Parses the given string of HTML without sanitizing it and returns a new HTMLDocument.
139      *
140      * @param cx the current context
141      * @param scope the scope
142      * @param thisObj the scriptable this object
143      * @param args the arguments
144      * @param funObj the function object
145      * @return a newly created {@link HTMLDocument}
146      *
147      * @see <a href="https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-parsehtmlunsafe">
148      *     HTML spec - parseHTMLUnsafe</a>
149      */
150     @JsxStaticFunction
151     public static HTMLDocument parseHTMLUnsafe(final Context cx, final VarScope scope,
152             final Scriptable thisObj, final Object[] args, final Function funObj) {
153         return (HTMLDocument) Document.parseHTMLUnsafe(cx, scope, thisObj, args, funObj);
154     }
155 
156     /**
157      * JavaScript function "write" may accept a variable number of arguments.
158      * @param context the JavaScript context
159      * @param scope the scope
160      * @param thisObj the scriptable
161      * @param args the arguments passed into the method
162      * @param function the function
163      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/write">MDN Documentation</a>
164      */
165     @JsxFunction
166     public static void write(final Context context, final VarScope scope,
167             final Scriptable thisObj, final Object[] args, final Function function) {
168         final HTMLDocument thisAsDocument = getDocument(thisObj);
169         thisAsDocument.write(concatArgsAsString(args));
170     }
171 
172     /**
173      * Converts the arguments to strings and concatenate them.
174      * @param args the JavaScript arguments
175      * @return the string concatenation
176      */
177     private static String concatArgsAsString(final Object[] args) {
178         final StringBuilder builder = new StringBuilder();
179         for (final Object arg : args) {
180             builder.append(JavaScriptEngine.toString(arg));
181         }
182         return builder.toString();
183     }
184 
185     /**
186      * Moves a given Node inside the invoking node as a direct child, before a given reference node.
187      *
188      * @param context the JavaScript context
189      * @param scope the scope
190      * @param thisObj the scriptable
191      * @param args the arguments passed into the method
192      * @param function the function
193      */
194     @JsxFunction
195     public static void moveBefore(final Context context, final VarScope scope,
196             final Scriptable thisObj, final Object[] args, final Function function) {
197         Node.moveBefore(context, scope, thisObj, args, function);
198     }
199 
200     /**
201      * JavaScript function "writeln" may accept a variable number of arguments.
202      * @param context the JavaScript context
203      * @param scope the scope
204      * @param thisObj the scriptable
205      * @param args the arguments passed into the method
206      * @param function the function
207      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/writeln">MDN Documentation</a>
208      */
209     @JsxFunction
210     public static void writeln(final Context context, final VarScope scope,
211             final Scriptable thisObj, final Object[] args, final Function function) {
212         final HTMLDocument thisAsDocument = getDocument(thisObj);
213         thisAsDocument.write(concatArgsAsString(args) + "\n");
214     }
215 
216     /**
217      * Returns the current document instance, using <code>thisObj</code> as a hint.
218      * @param thisObj a hint as to the current document (maybe the prototype when function is used without "this")
219      * @return the current document instance
220      */
221     private static HTMLDocument getDocument(final Scriptable thisObj) {
222         // if function is used "detached", then thisObj is the top scope (ie Window), not the real object
223         // cf unit test DocumentTest#testDocumentWrite_AssignedToVar
224         // may be the prototype too
225         // cf DocumentTest#testDocumentWrite_AssignedToVar2
226         if (thisObj instanceof HTMLDocument document && thisObj.getPrototype() instanceof HTMLDocument) {
227             return document;
228         }
229         if (thisObj instanceof DocumentProxy proxy && thisObj.getPrototype() instanceof HTMLDocument) {
230             return (HTMLDocument) proxy.getDelegee();
231         }
232 
233         throw JavaScriptEngine.reportRuntimeError("Function can't be used detached from document");
234     }
235 
236     /**
237      * This a hack!!! A cleaner way is welcome.
238      * Handle a case where document.write() is simply ignored.
239      * See HTMLDocumentWrite2Test.write_fromScriptAddedWithAppendChild_external.
240      * @param executing indicates if executing or not
241      */
242     public void setExecutingDynamicExternalPosponed(final boolean executing) {
243         executionExternalPostponed_ = executing;
244     }
245 
246     /**
247      * JavaScript function "write".
248      * <p>
249      * See http://www.whatwg.org/specs/web-apps/current-work/multipage/section-dynamic.html for
250      * a good description of the semantics of open(), write(), writeln() and close().
251      * </p>
252      *
253      * @param content the content to write
254      */
255     protected void write(final String content) {
256         // really strange: if called from an external script loaded as postponed action, write is ignored!!!
257         if (executionExternalPostponed_) {
258             if (LOG.isDebugEnabled()) {
259                 LOG.debug("skipping write for external posponed: " + content);
260             }
261             return;
262         }
263 
264         if (LOG.isDebugEnabled()) {
265             LOG.debug("write: " + content);
266         }
267 
268         final HtmlPage page = (HtmlPage) getDomNodeOrDie();
269         if (!page.isBeingParsed()) {
270             writeInCurrentDocument_ = false;
271         }
272 
273         // Add content to the content buffer.
274         writeBuilder_.append(content);
275 
276         // If open() was called; don't write to doc yet -- wait for call to close().
277         if (!writeInCurrentDocument_) {
278             LOG.debug("wrote content to buffer");
279             scheduleImplicitClose();
280             return;
281         }
282         final String bufferedContent = writeBuilder_.toString();
283         if (!canAlreadyBeParsed(bufferedContent)) {
284             LOG.debug("write: not enough content to parse it now");
285             return;
286         }
287 
288         writeBuilder_.setLength(0);
289         page.writeInParsedStream(bufferedContent);
290     }
291 
292     private void scheduleImplicitClose() {
293         if (!closePostponedAction_) {
294             closePostponedAction_ = true;
295             final HtmlPage page = (HtmlPage) getDomNodeOrDie();
296             final WebWindow enclosingWindow = page.getEnclosingWindow();
297             page.getWebClient().getJavaScriptEngine().addPostponedAction(
298                     new PostponedAction(page, "HTMLDocument.scheduleImplicitClose") {
299                         @Override
300                         public void execute() throws Exception {
301                             if (writeBuilder_.length() != 0) {
302                                 close();
303                             }
304                             closePostponedAction_ = false;
305                         }
306 
307                         @Override
308                         public boolean isStillAlive() {
309                             return !enclosingWindow.isClosed();
310                         }
311                     });
312         }
313     }
314 
315     /**
316      * Indicates if the content is a well-formed HTML snippet that can already be parsed to be added to the DOM.
317      *
318      * @param content the HTML snippet
319      * @return {@code false} if it is not well-formed
320      */
321     static boolean canAlreadyBeParsed(final String content) {
322         // all <script> must have their </script> because the parser doesn't close automatically this tag
323         // All tags must be complete, that is from '<' to '>'.
324         ParsingStatus tagState = ParsingStatus.OUTSIDE;
325         int tagNameBeginIndex = 0;
326         int scriptTagCount = 0;
327         boolean tagIsOpen = true;
328         char stringBoundary = 0;
329         boolean stringSkipNextChar = false;
330         int index = 0;
331         char openingQuote = 0;
332         for (final char currentChar : content.toCharArray()) {
333             switch (tagState) {
334                 case OUTSIDE:
335                     if (currentChar == '<') {
336                         tagState = ParsingStatus.START;
337                         tagIsOpen = true;
338                     }
339                     else if (scriptTagCount > 0 && (currentChar == '\'' || currentChar == '"')) {
340                         tagState = ParsingStatus.IN_STRING;
341                         stringBoundary = currentChar;
342                         stringSkipNextChar = false;
343                     }
344                     break;
345                 case START:
346                     if (currentChar == '/') {
347                         tagIsOpen = false;
348                         tagNameBeginIndex = index + 1;
349                     }
350                     else {
351                         tagNameBeginIndex = index;
352                     }
353                     tagState = ParsingStatus.IN_NAME;
354                     break;
355                 case IN_NAME:
356                     if (Character.isWhitespace(currentChar) || currentChar == '>') {
357                         final String tagName = content.substring(tagNameBeginIndex, index);
358                         if ("script".equalsIgnoreCase(tagName)) {
359                             if (tagIsOpen) {
360                                 scriptTagCount++;
361                             }
362                             else if (scriptTagCount > 0) {
363                                 // Ignore extra close tags for now. Let the parser deal with them.
364                                 scriptTagCount--;
365                             }
366                         }
367                         if (currentChar == '>') {
368                             tagState = ParsingStatus.OUTSIDE;
369                         }
370                         else {
371                             tagState = ParsingStatus.INSIDE;
372                         }
373                     }
374                     else if (!Character.isLetter(currentChar)) {
375                         tagState = ParsingStatus.OUTSIDE;
376                     }
377                     break;
378                 case INSIDE:
379                     if (currentChar == openingQuote) {
380                         openingQuote = 0;
381                     }
382                     else if (openingQuote == 0) {
383                         if (currentChar == '\'' || currentChar == '"') {
384                             openingQuote = currentChar;
385                         }
386                         else if (currentChar == '>' && openingQuote == 0) {
387                             tagState = ParsingStatus.OUTSIDE;
388                         }
389                     }
390                     break;
391                 case IN_STRING:
392                     if (stringSkipNextChar) {
393                         stringSkipNextChar = false;
394                     }
395                     else {
396                         if (currentChar == stringBoundary) {
397                             tagState = ParsingStatus.OUTSIDE;
398                         }
399                         else if (currentChar == '\\') {
400                             stringSkipNextChar = true;
401                         }
402                     }
403                     break;
404                 default:
405                     // nothing
406             }
407             index++;
408         }
409         if (scriptTagCount > 0 || tagState != ParsingStatus.OUTSIDE) {
410             if (LOG.isDebugEnabled()) {
411                 final StringBuilder message = new StringBuilder()
412                     .append("canAlreadyBeParsed() returns false for content: '")
413                     .append(StringUtils.abbreviateMiddle(content, ".", 100))
414                     .append("' (scriptTagCount: ")
415                         .append(scriptTagCount)
416                     .append(" tagState: ")
417                         .append(tagState)
418                     .append(')');
419                 LOG.debug(message.toString());
420             }
421             return false;
422         }
423 
424         return true;
425     }
426 
427     /**
428      * Gets the node that is the last one when exploring following nodes, depth-first.
429      * @param node the node to search
430      * @return the searched node
431      */
432     HtmlElement getLastHtmlElement(final HtmlElement node) {
433         final DomNode lastChild = node.getLastChild();
434         if (!(lastChild instanceof HtmlElement)
435                 || lastChild instanceof HtmlScript) {
436             return node;
437         }
438 
439         return getLastHtmlElement((HtmlElement) lastChild);
440     }
441 
442     /**
443      * JavaScript function "open".
444      * <p>
445      * See http://www.whatwg.org/specs/web-apps/current-work/multipage/section-dynamic.html for
446      * a good description of the semantics of open(), write(), writeln() and close().
447      * </p>
448      *
449      * @param url when a new document is opened, <i>url</i> is a String that specifies a MIME type for the document.
450      *        When a new window is opened, <i>url</i> is a String that specifies the URL to render in the new window
451      * @param name the name
452      * @param features the features
453      * @param replace whether to replace in the history list or not
454      * @return a reference to the new document object
455      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/open">MDN Documentation</a>
456      */
457     @JsxFunction
458     public HTMLDocument open(final Object url, final Object name, final Object features,
459             final Object replace) {
460         // Any open() invocations are ignored during the parsing stage, because write() and
461         // writeln() invocations will directly append content to the current insertion point.
462         final HtmlPage page = getPage();
463         if (page.isBeingParsed()) {
464             LOG.warn("Ignoring call to open() during the parsing stage.");
465             return null;
466         }
467 
468         // We're not in the parsing stage; OK to continue.
469         if (!writeInCurrentDocument_) {
470             LOG.warn("Function open() called when document is already open.");
471         }
472         writeInCurrentDocument_ = false;
473         final WebWindow ww = getWindow().getWebWindow();
474         if (ww instanceof FrameWindow window
475                 && UrlUtils.ABOUT_BLANK.equals(getPage().getUrl().toExternalForm())) {
476             final URL enclosingUrl = window.getEnclosingPage().getUrl();
477             getPage().getWebResponse().getWebRequest().setUrl(enclosingUrl);
478         }
479         return this;
480     }
481 
482     /**
483      * {@inheritDoc}
484      */
485     @Override
486     @JsxFunction({FF, FF_ESR})
487     public void close() throws IOException {
488         if (writeInCurrentDocument_) {
489             LOG.warn("close() called when document is not open.");
490         }
491         else {
492             final HtmlPage page = getPage();
493             final URL url = page.getUrl();
494             final StringWebResponse webResponse = new StringWebResponse(writeBuilder_.toString(), url);
495             webResponse.setFromJavascript(true);
496             writeInCurrentDocument_ = true;
497             writeBuilder_.setLength(0);
498 
499             final WebClient webClient = page.getWebClient();
500             final WebWindow window = page.getEnclosingWindow();
501             // reset isAttachedToPageDuringOnload_ to trigger the onload event for chrome also
502             if (window instanceof FrameWindow frameWindow) {
503                 final BaseFrameElement frame = frameWindow.getFrameElement();
504                 final HtmlUnitScriptable scriptable = frame.getScriptableObject();
505                 if (scriptable instanceof HTMLIFrameElement element) {
506                     element.onRefresh();
507                 }
508             }
509             webClient.loadWebResponseInto(webResponse, window);
510         }
511     }
512 
513     /**
514      * {@inheritDoc}
515      */
516     @JsxGetter
517     @Override
518     public Element getDocumentElement() {
519         implicitCloseIfNecessary();
520         return super.getDocumentElement();
521     }
522 
523     /**
524      * Closes the document implicitly, i.e. flushes the <code>document.write</code> buffer (IE only).
525      */
526     private void implicitCloseIfNecessary() {
527         if (!writeInCurrentDocument_) {
528             try {
529                 close();
530             }
531             catch (final IOException e) {
532                 throw JavaScriptEngine.throwAsScriptRuntimeEx(e);
533             }
534         }
535     }
536 
537     /**
538      * {@inheritDoc}
539      */
540     @Override
541     public Node appendChild(final Object childObject) {
542         if (childObject instanceof Node childNode
543                 && !isInsertableAtEnd(getPage(), childNode.getDomNodeOrDie())) {
544             throw JavaScriptEngine.asJavaScriptException(
545                     getWindow(),
546                     "Node cannot be inserted at the specified point in the hierarchy.",
547                     org.htmlunit.javascript.host.dom.DOMException.HIERARCHY_REQUEST_ERR);
548         }
549         return super.appendChild(childObject);
550     }
551 
552     /**
553      * Pre-insertion validity of a node that is appended to a document (DOM standard, "ensure
554      * pre-insertion validity"): at most one element, at most one doctype and the doctype has to be
555      * in front of the element; text is not allowed; comments and processing instructions are always fine.
556      */
557     private static boolean isInsertableAtEnd(final HtmlPage page, final DomNode node) {
558         if (node instanceof DomDocumentFragment fragment) {
559             int elements = 0;
560             for (final DomNode child : fragment.getChildren()) {
561                 if (child instanceof org.w3c.dom.Element) {
562                     elements++;
563                 }
564                 else if (child instanceof org.w3c.dom.Text) {
565                     return false;
566                 }
567             }
568             return elements == 0 || (elements == 1 && page.getDocumentElement() == null);
569         }
570         if (node instanceof org.w3c.dom.Element) {
571             return page.getDocumentElement() == null;
572         }
573         if (node instanceof org.w3c.dom.DocumentType) {
574             return page.getDoctype() == null && page.getDocumentElement() == null;
575         }
576         return node instanceof org.w3c.dom.Comment || node instanceof org.w3c.dom.ProcessingInstruction;
577     }
578 
579     /**
580      * Returns the element with the specified ID, or {@code null} if that element could not be found.
581      * @param id the ID to search for
582      * @return the element, or {@code null} if it could not be found
583      */
584     @JsxFunction
585     @Override
586     public HtmlUnitScriptable getElementById(final String id) {
587         implicitCloseIfNecessary();
588         final DomElement domElement = getPage().getElementById(id);
589         if (domElement == null) {
590             // Just fall through - result is already set to null
591             if (LOG.isDebugEnabled()) {
592                 LOG.debug("getElementById(" + id + "): no DOM node found with this id");
593             }
594             return null;
595         }
596 
597         final HtmlUnitScriptable jsElement = getScriptableFor(domElement);
598         if (jsElement == NOT_FOUND) {
599             if (LOG.isDebugEnabled()) {
600                 LOG.debug("getElementById(" + id
601                         + ") cannot return a result as there isn't a JavaScript object for the HTML element "
602                         + domElement.getClass().getName());
603             }
604             return null;
605         }
606         return jsElement;
607     }
608 
609     /**
610      * {@inheritDoc}
611      */
612     @Override
613     public HTMLCollection getElementsByClassName(final String className) {
614         return getDocumentElement().getElementsByClassName(className);
615     }
616 
617     /**
618      * {@inheritDoc}
619      */
620     @Override
621     public NodeList getElementsByName(final String elementName) {
622         implicitCloseIfNecessary();
623 
624         if ("null".equals(elementName)
625                 || (elementName.isEmpty()
626                     && getBrowserVersion().hasFeature(HTMLDOCUMENT_ELEMENTS_BY_NAME_EMPTY))) {
627             return NodeList.staticNodeList(getParentScope(), new ArrayList<>());
628         }
629 
630         final HtmlPage page = getPage();
631         final NodeList elements = new NodeList(page, true);
632         elements.setElementsSupplier(
633                 (Supplier<List<DomNode>> & Serializable)
634                 () -> new ArrayList<>(page.getElementsByName(elementName)));
635 
636         elements.setEffectOnCacheFunction(
637                 (java.util.function.Function<HtmlAttributeChangeEvent, EffectOnCache> & Serializable)
638                 event -> {
639                     if ("name".equals(event.getName())) {
640                         return EffectOnCache.RESET;
641                     }
642                     return EffectOnCache.NONE;
643                 });
644 
645         return elements;
646     }
647 
648     /**
649      * Calls to <code>document.XYZ</code> should first look at elements named <code>XYZ</code> before
650      * using standard functions.
651      *
652      * {@inheritDoc}
653      */
654     @Override
655     protected Object getWithPreemption(final String name) {
656         final HtmlPage page = (HtmlPage) getDomNodeOrNull();
657         if (page == null) {
658             final Object response = getPrototype().get(name, this);
659             if (response != NOT_FOUND) {
660                 return response;
661             }
662         }
663         return getIt(name);
664     }
665 
666     private Object getIt(final String name) {
667         final HtmlPage page = (HtmlPage) getDomNodeOrNull();
668         if (page == null) {
669             return NOT_FOUND;
670         }
671 
672         // for performance,
673         // we will calculate the elements to decide if we really have
674         // to really create a HTMLCollection or not
675         final List<DomNode> matchingElements = getItComputeElements(page, name);
676         final int size = matchingElements.size();
677         if (size == 0) {
678             return NOT_FOUND;
679         }
680         if (size == 1) {
681             final DomNode object = matchingElements.get(0);
682             if (object instanceof BaseFrameElement element) {
683                 return element.getEnclosedWindow().getScriptableObject();
684             }
685             return super.getScriptableFor(object);
686         }
687 
688         final HTMLCollection coll = new HTMLCollection(page, matchingElements) {
689             @Override
690             protected HtmlUnitScriptable getScriptableFor(final Object object) {
691                 if (object instanceof BaseFrameElement element) {
692                     return element.getEnclosedWindow().getScriptableObject();
693                 }
694                 return super.getScriptableFor(object);
695             }
696         };
697 
698         coll.setElementsSupplier(
699                 (Supplier<List<DomNode>> & Serializable)
700                 () -> getItComputeElements(page, name));
701 
702         coll.setEffectOnCacheFunction(
703                 (java.util.function.Function<HtmlAttributeChangeEvent, EffectOnCache> & Serializable)
704                 event -> {
705                     final String attributeName = event.getName();
706                     if (DomElement.NAME_ATTRIBUTE.equals(attributeName)) {
707                         return EffectOnCache.RESET;
708                     }
709 
710                     return EffectOnCache.NONE;
711                 });
712 
713         return coll;
714     }
715 
716     static List<DomNode> getItComputeElements(final HtmlPage page, final String name) {
717         final List<DomElement> elements = page.getElementsByName(name);
718         final List<DomNode> matchingElements = new ArrayList<>();
719         for (final DomElement elt : elements) {
720             if (elt instanceof HtmlForm || elt instanceof HtmlImage || elt instanceof BaseFrameElement) {
721                 matchingElements.add(elt);
722             }
723         }
724         return matchingElements;
725     }
726 
727     /**
728      * {@inheritDoc}
729      */
730     @Override
731     public HTMLElement getHead() {
732         final HtmlElement head = getPage().getHead();
733         if (head == null) {
734             return null;
735         }
736         return head.getScriptableObject();
737     }
738 
739     /**
740      * {@inheritDoc}
741      */
742     @Override
743     public String getTitle() {
744         return getPage().getDocumentTitle();
745     }
746 
747     /**
748      * {@inheritDoc}
749      */
750     @Override
751     public void setTitle(final String title) {
752         getPage().setTitleText(title);
753     }
754 
755     /**
756      * {@inheritDoc}
757      */
758     @Override
759     public HTMLElement getActiveElement() {
760         final HtmlElement activeElement = getPage().getActiveElement();
761         if (activeElement != null) {
762             return activeElement.getScriptableObject();
763         }
764         return null;
765     }
766 
767     /**
768      * {@inheritDoc}
769      */
770     @Override
771     public boolean hasFocus() {
772         return getPage().getFocusedElement() != null;
773     }
774 
775     /**
776      * Dispatches an event into the event system (standards-conformant browsers only). See
777      * <a href="https://developer.mozilla.org/en-US/docs/DOM/element.dispatchEvent">the Gecko
778      * DOM reference</a> for more information.
779      *
780      * @param event the event to be dispatched
781      * @return {@code false} if at least one of the event handlers which handled the event
782      *         called <code>preventDefault</code>; {@code true} otherwise
783      */
784     @Override
785     @JsxFunction
786     public boolean dispatchEvent(final Event event) {
787         event.setTarget(this);
788         final ScriptResult result = fireEvent(event);
789         return !event.isAborted(result);
790     }
791 
792     /**
793      * {@inheritDoc}
794      */
795     @Override
796     public Selection getSelection() {
797         return getWindow().getSelectionImpl();
798     }
799 
800     /**
801      * Creates a new HTML attribute with the specified name.
802      *
803      * @param attributeName the name of the attribute to create
804      * @return an attribute with the specified name
805      */
806     @Override
807     public Attr createAttribute(final String attributeName) {
808         String name = attributeName;
809         if (!org.htmlunit.util.StringUtils.isEmptyOrNull(name)) {
810             name = org.htmlunit.util.StringUtils.toRootLowerCase(name);
811         }
812 
813         return super.createAttribute(name);
814     }
815 
816     /**
817      * {@inheritDoc}
818      */
819     @Override
820     public String getBaseURI() {
821         return getPage().getBaseURL().toString();
822     }
823 
824     /**
825      * {@inheritDoc}
826      */
827     @Override
828     public HtmlUnitScriptable elementFromPoint(final int x, final int y) {
829         final HtmlElement element = getPage().getElementFromPoint(x, y);
830         return element == null ? null : element.getScriptableObject();
831     }
832 }