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;
16  
17  import static org.htmlunit.BrowserVersionFeatures.HTMLIMAGE_HTMLELEMENT;
18  import static org.htmlunit.BrowserVersionFeatures.HTMLIMAGE_HTMLUNKNOWNELEMENT;
19  
20  import java.io.IOException;
21  import java.util.function.Supplier;
22  
23  import org.apache.commons.lang3.function.FailableSupplier;
24  import org.apache.commons.logging.Log;
25  import org.apache.commons.logging.LogFactory;
26  import org.htmlunit.BrowserVersion;
27  import org.htmlunit.WebAssert;
28  import org.htmlunit.WebWindow;
29  import org.htmlunit.corejs.javascript.Context;
30  import org.htmlunit.corejs.javascript.JSFunction;
31  import org.htmlunit.corejs.javascript.NativePromise;
32  import org.htmlunit.corejs.javascript.Scriptable;
33  import org.htmlunit.corejs.javascript.ScriptableObject;
34  import org.htmlunit.corejs.javascript.TopLevel;
35  import org.htmlunit.corejs.javascript.VarScope;
36  import org.htmlunit.html.DomNode;
37  import org.htmlunit.html.HtmlImage;
38  import org.htmlunit.javascript.host.Window;
39  import org.htmlunit.javascript.host.WindowOrWorkerGlobalScope;
40  import org.htmlunit.javascript.host.html.HTMLElement;
41  import org.htmlunit.javascript.host.html.HTMLUnknownElement;
42  
43  /**
44   * Base class for Rhino host objects in HtmlUnit (not bound to a DOM node).
45   *
46   * @author Mike Bowler
47   * @author David K. Taylor
48   * @author Marc Guillemot
49   * @author Chris Erskine
50   * @author Daniel Gredler
51   * @author Ahmed Ashour
52   * @author Ronald Brill
53   * @author Sven Strickroth
54   */
55  public class HtmlUnitScriptable extends ScriptableObject implements Cloneable {
56  
57      private static final Log LOG = LogFactory.getLog(HtmlUnitScriptable.class);
58  
59      private DomNode domNode_;
60      private String className_;
61  
62      /**
63       * Returns the JavaScript class name.
64       * @return the JavaScript class name
65       */
66      @Override
67      public String getClassName() {
68          if (className_ != null) {
69              return className_;
70          }
71          if (getPrototype() != null) {
72              return getPrototype().getClassName();
73          }
74          String className = getClass().getSimpleName();
75          if (className.isEmpty()) {
76              // for anonymous class
77              className = getClass().getSuperclass().getSimpleName();
78          }
79          return className;
80      }
81  
82      /**
83       * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
84       *
85       * Sets the class name.
86       * @param className the class name.
87       */
88      public void setClassName(final String className) {
89          className_ = className;
90      }
91  
92      /**
93       * {@inheritDoc}
94       */
95      @Override
96      public void put(final String name, final Scriptable start, final Object value) {
97          try {
98              super.put(name, start, value);
99          }
100         catch (final IllegalArgumentException e) {
101             // is it the right place or should Rhino throw a RuntimeError instead of an IllegalArgumentException?
102             throw JavaScriptEngine.typeError("'set "
103                 + name + "' called on an object that does not implement interface " + getClassName());
104         }
105     }
106 
107     /**
108      * Gets a named property from the object.
109      * Normally HtmlUnit objects don't need to overwrite this method as properties are defined
110      * on the prototypes. In some cases where "content" of object
111      * has priority compared to the properties consider using utility {@link #getWithPreemption(String)}.
112      * {@inheritDoc}
113      */
114     @Override
115     public Object get(final String name, final Scriptable start) {
116         // Try to get property configured on object itself.
117         final Object response = super.get(name, start);
118         if (response == NOT_FOUND && this == start) {
119             return getWithPreemption(name);
120         }
121         return response;
122     }
123 
124     /**
125      * <p>Called by {@link #get(String, Scriptable)} to allow retrieval of the property before the prototype
126      * chain is searched.</p>
127      *
128      * <p>IMPORTANT: This method is invoked *very* often by Rhino. If you override this method, the implementation
129      * needs to be as fast as possible!</p>
130      *
131      * @param name the property name
132      * @return {@link Scriptable#NOT_FOUND} if not found
133      */
134     protected Object getWithPreemption(final String name) {
135         return NOT_FOUND;
136     }
137 
138     @Override
139     public boolean has(final int index, final Scriptable start) {
140         final Object found = get(index, start);
141         if (Scriptable.NOT_FOUND != found && !JavaScriptEngine.isUndefined(found)) {
142             return true;
143         }
144         return super.has(index, start);
145     }
146 
147     /**
148      * Returns the DOM node that corresponds to this JavaScript object or throw
149      * an exception if one cannot be found.
150      * @return the DOM node
151      */
152     public DomNode getDomNodeOrDie() {
153         if (domNode_ == null) {
154             throw new IllegalStateException("DomNode has not been set for this HtmlUnitScriptable: "
155                         + getClass().getName());
156         }
157         return domNode_;
158     }
159 
160     /**
161      * Returns the DOM node that corresponds to this JavaScript object
162      * or null if a node hasn't been set.
163      * @return the DOM node or null
164      */
165     public DomNode getDomNodeOrNull() {
166         return domNode_;
167     }
168 
169     /**
170      * Sets the DOM node that corresponds to this JavaScript object.
171      * @param domNode the DOM node
172      */
173     public void setDomNode(final DomNode domNode) {
174         setDomNode(domNode, true);
175     }
176 
177     /**
178      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
179      *
180      * Sets the DOM node that corresponds to this JavaScript object.
181      * @param domNode the DOM node
182      * @param assignScriptObject If true, call <code>setScriptObject</code> on domNode
183      */
184     public void setDomNode(final DomNode domNode, final boolean assignScriptObject) {
185         WebAssert.notNull("domNode", domNode);
186         domNode_ = domNode;
187         if (assignScriptObject) {
188             domNode_.setScriptableObject(this);
189         }
190     }
191 
192     /**
193      * Returns the JavaScript object that corresponds to the specified object.
194      * New JavaScript objects will be created as needed. If a JavaScript object
195      * cannot be created for a domNode then NOT_FOUND will be returned.
196      *
197      * @param object a {@link DomNode} or a {@link WebWindow}
198      * @return the JavaScript object or NOT_FOUND
199      */
200     protected HtmlUnitScriptable getScriptableFor(final Object object) {
201         if (object instanceof WebWindow window) {
202             return window.getScriptableObject();
203         }
204 
205         final DomNode domNode = (DomNode) object;
206 
207         final HtmlUnitScriptable scriptObject = domNode.getScriptableObject();
208         if (scriptObject != null) {
209             return scriptObject;
210         }
211         return makeScriptableFor(domNode);
212     }
213 
214     /**
215      * Builds a new the JavaScript object that corresponds to the specified object.
216      * @param domNode the DOM node for which a JS object should be created
217      * @return the JavaScript object
218      */
219     public HtmlUnitScriptable makeScriptableFor(final DomNode domNode) {
220         // Get the JS class name for the specified DOM node.
221         // Walk up the inheritance chain if necessary.
222         Class<? extends HtmlUnitScriptable> javaScriptClass = null;
223         if (domNode instanceof HtmlImage image && "image".equals(image.getOriginalQualifiedName())
224                 && image.wasCreatedByJavascript()) {
225             if (domNode.hasFeature(HTMLIMAGE_HTMLELEMENT)) {
226                 javaScriptClass = HTMLElement.class;
227             }
228             else if (domNode.hasFeature(HTMLIMAGE_HTMLUNKNOWNELEMENT)) {
229                 javaScriptClass = HTMLUnknownElement.class;
230             }
231         }
232         if (javaScriptClass == null) {
233             final JavaScriptEngine javaScriptEngine =
234                     (JavaScriptEngine) getWindow().getWebWindow().getWebClient().getJavaScriptEngine();
235             for (Class<?> c = domNode.getClass(); javaScriptClass == null && c != null; c = c.getSuperclass()) {
236                 javaScriptClass = javaScriptEngine.getJavaScriptClass(c);
237             }
238         }
239 
240         final HtmlUnitScriptable scriptable;
241         if (javaScriptClass == null) {
242             // We don't have a specific subclass for this element so create something generic.
243             scriptable = new HTMLElement();
244             if (LOG.isDebugEnabled()) {
245                 LOG.debug("No JavaScript class found for element <" + domNode.getNodeName() + ">. Using HTMLElement");
246             }
247         }
248         else {
249             try {
250                 scriptable = javaScriptClass.getDeclaredConstructor().newInstance();
251             }
252             catch (final Exception e) {
253                 throw JavaScriptEngine.throwAsScriptRuntimeEx(e);
254             }
255         }
256 
257         scriptable.setParentScope(getParentScope());
258         scriptable.setPrototype(getPrototype(javaScriptClass));
259         scriptable.setDomNode(domNode);
260 
261         return scriptable;
262     }
263 
264     /**
265      * Gets the prototype object for the given host class.
266      * @param javaScriptClass the host class
267      * @return the prototype
268      */
269     @SuppressWarnings("unchecked")
270     public Scriptable getPrototype(final Class<? extends HtmlUnitScriptable> javaScriptClass) {
271         final Scriptable prototype = getWindow().getPrototype(javaScriptClass);
272         if (prototype == null && javaScriptClass != HtmlUnitScriptable.class) {
273             return getPrototype((Class<? extends HtmlUnitScriptable>) javaScriptClass.getSuperclass());
274         }
275         return prototype;
276     }
277 
278     /**
279      * Returns the JavaScript default value of this object. This is the JavaScript equivalent of a toString() in Java.
280      *
281      * @param hint a hint as to the format of the default value (ignored in this case)
282      * @return the default value
283      */
284     @Override
285     public Object getDefaultValue(final Class<?> hint) {
286         if (String.class.equals(hint) || hint == null) {
287             return "[object " + getClassName() + "]";
288         }
289         return super.getDefaultValue(hint);
290     }
291 
292     /**
293      * Gets the window that is the top scope for this object.
294      * @return the window associated with this object
295      * @throws RuntimeException if the window cannot be found, which should never occur
296      */
297     public Window getWindow() throws RuntimeException {
298         return getWindow(this);
299     }
300 
301     /**
302      * Gets the window that is the top scope for the specified object.
303      * @param s the JavaScript object whose associated window is to be returned
304      * @return the window associated with the specified JavaScript object
305      * @throws RuntimeException if the window cannot be found, which should never occur
306      */
307     protected static Window getWindow(final Scriptable s) throws RuntimeException {
308         if (s instanceof Window window) {
309             return window;
310         }
311 
312         final TopLevel topLevel = ScriptableObject.getTopLevelScope(s.getParentScope());
313         if (topLevel.getGlobalThis() instanceof Window window) {
314             return window;
315         }
316         throw new RuntimeException("Unable to find window associated with " + s);
317     }
318 
319     protected static WindowOrWorkerGlobalScope getWindowOrWorkerGlobalScope(
320                         final Scriptable s) throws RuntimeException {
321         if (s instanceof WindowOrWorkerGlobalScope wow) {
322             return wow;
323         }
324 
325         final TopLevel topLevel = ScriptableObject.getTopLevelScope(s.getParentScope());
326         if (topLevel.getGlobalThis() instanceof WindowOrWorkerGlobalScope wow) {
327             return wow;
328         }
329         throw new RuntimeException("Unable to find WindowOrWorkerGlobalScope associated with " + s);
330     }
331 
332     /**
333      * Gets the browser version currently used.
334      * @return the browser version
335      */
336     public BrowserVersion getBrowserVersion() {
337         final DomNode node = getDomNodeOrNull();
338         if (node != null) {
339             return node.getPage().getWebClient().getBrowserVersion();
340         }
341 
342         final Window window = getWindow();
343         if (window != null) {
344             final WebWindow webWindow = window.getWebWindow();
345             if (webWindow != null) {
346                 return webWindow.getWebClient().getBrowserVersion();
347             }
348         }
349 
350         return null;
351     }
352 
353     /**
354      * {@inheritDoc}
355      */
356     @Override
357     public boolean hasInstance(final Scriptable instance) {
358         if (getPrototype() == null) {
359             // to handle cases like "x instanceof HTMLElement",
360             // but HTMLElement is not in the prototype chain of any element
361             final Object prototype = get("prototype", this);
362             if (!(prototype instanceof ScriptableObject)) {
363                 throw JavaScriptEngine.throwAsScriptRuntimeEx(new Exception("Null prototype"));
364             }
365             return ((ScriptableObject) prototype).hasInstance(instance);
366         }
367 
368         return super.hasInstance(instance);
369     }
370 
371     /**
372      * {@inheritDoc}
373      */
374     @Override
375     protected Object equivalentValues(Object value) {
376         if (value instanceof HtmlUnitScriptableProxy<?> proxy) {
377             value = proxy.getDelegee();
378         }
379         return super.equivalentValues(value);
380     }
381 
382     /**
383      * {@inheritDoc}
384      */
385     @Override
386     public HtmlUnitScriptable clone() {
387         try {
388             return (HtmlUnitScriptable) super.clone();
389         }
390         catch (final Exception e) {
391             throw new IllegalStateException("Clone not supported");
392         }
393     }
394 
395     protected NativePromise setupPromise(final FailableSupplier<Object, IOException> resolver) {
396         final VarScope scope = ScriptableObject.getTopLevelScope(getParentScope());
397         final JSFunction ctor = (JSFunction) getProperty(scope, "Promise");
398 
399         try {
400             final JSFunction resolve = (JSFunction) getProperty(ctor, "resolve");
401             return (NativePromise) resolve.call(Context.getCurrentContext(), scope,
402                                                 ctor, new Object[] {resolver.get()});
403         }
404         catch (final IOException e) {
405             final JSFunction reject = (JSFunction) getProperty(ctor, "reject");
406             return (NativePromise) reject.call(Context.getCurrentContext(), scope, ctor, new Object[] {e.getMessage()});
407         }
408     }
409 
410     protected NativePromise setupRejectedPromise(final Supplier<Object> resolver) {
411         final VarScope scope = ScriptableObject.getTopLevelScope(getParentScope());
412         final JSFunction ctor = (JSFunction) getProperty(scope, "Promise");
413         final JSFunction reject = (JSFunction) getProperty(ctor, "reject");
414         return (NativePromise) reject.call(Context.getCurrentContext(), scope, ctor, new Object[] {resolver.get()});
415     }
416 }