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      * <p>
113      * {@inheritDoc}
114      */
115     @Override
116     public Object get(final String name, final Scriptable start) {
117         // Try to get property configured on object itself.
118         final Object response = super.get(name, start);
119         if (response == NOT_FOUND && this == start) {
120             return getWithPreemption(name);
121         }
122         return response;
123     }
124 
125     /**
126      * <p>Called by {@link #get(String, Scriptable)} to allow retrieval of the property before the prototype
127      * chain is searched.</p>
128      *
129      * <p>IMPORTANT: This method is invoked *very* often by Rhino. If you override this method, the implementation
130      * needs to be as fast as possible!</p>
131      *
132      * @param name the property name
133      * @return {@link Scriptable#NOT_FOUND} if not found
134      */
135     protected Object getWithPreemption(final String name) {
136         return NOT_FOUND;
137     }
138 
139     @Override
140     public boolean has(final int index, final Scriptable start) {
141         final Object found = get(index, start);
142         if (Scriptable.NOT_FOUND != found && !JavaScriptEngine.isUndefined(found)) {
143             return true;
144         }
145         return super.has(index, start);
146     }
147 
148     /**
149      * Returns the DOM node that corresponds to this JavaScript object or throw
150      * an exception if one cannot be found.
151      * @return the DOM node
152      */
153     public DomNode getDomNodeOrDie() {
154         if (domNode_ == null) {
155             throw new IllegalStateException("DomNode has not been set for this HtmlUnitScriptable: "
156                         + getClass().getName());
157         }
158         return domNode_;
159     }
160 
161     /**
162      * Returns the DOM node that corresponds to this JavaScript object
163      * or null if a node hasn't been set.
164      * @return the DOM node or null
165      */
166     public DomNode getDomNodeOrNull() {
167         return domNode_;
168     }
169 
170     /**
171      * Sets the DOM node that corresponds to this JavaScript object.
172      * @param domNode the DOM node
173      */
174     public void setDomNode(final DomNode domNode) {
175         setDomNode(domNode, true);
176     }
177 
178     /**
179      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
180      *
181      * Sets the DOM node that corresponds to this JavaScript object.
182      * @param domNode the DOM node
183      * @param assignScriptObject If true, call <code>setScriptObject</code> on domNode
184      */
185     public void setDomNode(final DomNode domNode, final boolean assignScriptObject) {
186         WebAssert.notNull("domNode", domNode);
187         domNode_ = domNode;
188         if (assignScriptObject) {
189             domNode_.setScriptableObject(this);
190         }
191     }
192 
193     /**
194      * Returns the JavaScript object that corresponds to the specified object.
195      * New JavaScript objects will be created as needed. If a JavaScript object
196      * cannot be created for a domNode then NOT_FOUND will be returned.
197      *
198      * @param object a {@link DomNode} or a {@link WebWindow}
199      * @return the JavaScript object or NOT_FOUND
200      */
201     protected HtmlUnitScriptable getScriptableFor(final Object object) {
202         if (object instanceof WebWindow window) {
203             return window.getScriptableObject();
204         }
205 
206         final DomNode domNode = (DomNode) object;
207 
208         final HtmlUnitScriptable scriptObject = domNode.getScriptableObject();
209         if (scriptObject != null) {
210             return scriptObject;
211         }
212         return makeScriptableFor(domNode);
213     }
214 
215     /**
216      * Builds a new the JavaScript object that corresponds to the specified object.
217      * @param domNode the DOM node for which a JS object should be created
218      * @return the JavaScript object
219      */
220     public HtmlUnitScriptable makeScriptableFor(final DomNode domNode) {
221         // Get the JS class name for the specified DOM node.
222         // Walk up the inheritance chain if necessary.
223         Class<? extends HtmlUnitScriptable> javaScriptClass = null;
224         if (domNode instanceof HtmlImage image && "image".equals(image.getOriginalQualifiedName())
225                 && image.wasCreatedByJavascript()) {
226             if (domNode.hasFeature(HTMLIMAGE_HTMLELEMENT)) {
227                 javaScriptClass = HTMLElement.class;
228             }
229             else if (domNode.hasFeature(HTMLIMAGE_HTMLUNKNOWNELEMENT)) {
230                 javaScriptClass = HTMLUnknownElement.class;
231             }
232         }
233         if (javaScriptClass == null) {
234             final JavaScriptEngine javaScriptEngine =
235                     (JavaScriptEngine) getWindow().getWebWindow().getWebClient().getJavaScriptEngine();
236             for (Class<?> c = domNode.getClass(); javaScriptClass == null && c != null; c = c.getSuperclass()) {
237                 javaScriptClass = javaScriptEngine.getJavaScriptClass(c);
238             }
239         }
240 
241         final HtmlUnitScriptable scriptable;
242         if (javaScriptClass == null) {
243             // We don't have a specific subclass for this element so create something generic.
244             scriptable = new HTMLElement();
245             if (LOG.isDebugEnabled()) {
246                 LOG.debug("No JavaScript class found for element <" + domNode.getNodeName() + ">. Using HTMLElement");
247             }
248         }
249         else {
250             try {
251                 scriptable = javaScriptClass.getDeclaredConstructor().newInstance();
252             }
253             catch (final Exception e) {
254                 throw JavaScriptEngine.throwAsScriptRuntimeEx(e);
255             }
256         }
257 
258         scriptable.setParentScope(getParentScope());
259         scriptable.setPrototype(getPrototype(javaScriptClass));
260         scriptable.setDomNode(domNode);
261 
262         return scriptable;
263     }
264 
265     /**
266      * Gets the prototype object for the given host class.
267      * @param javaScriptClass the host class
268      * @return the prototype
269      */
270     @SuppressWarnings("unchecked")
271     public Scriptable getPrototype(final Class<? extends HtmlUnitScriptable> javaScriptClass) {
272         final Scriptable prototype = getWindow().getPrototype(javaScriptClass);
273         if (prototype == null && javaScriptClass != HtmlUnitScriptable.class) {
274             return getPrototype((Class<? extends HtmlUnitScriptable>) javaScriptClass.getSuperclass());
275         }
276         return prototype;
277     }
278 
279     /**
280      * Returns the JavaScript default value of this object. This is the JavaScript equivalent of a toString() in Java.
281      *
282      * @param hint a hint as to the format of the default value (ignored in this case)
283      * @return the default value
284      */
285     @Override
286     public Object getDefaultValue(final Class<?> hint) {
287         if (String.class.equals(hint) || hint == null) {
288             return "[object " + getClassName() + "]";
289         }
290         return super.getDefaultValue(hint);
291     }
292 
293     /**
294      * Gets the window that is the top scope for this object.
295      * @return the window associated with this object
296      * @throws RuntimeException if the window cannot be found, which should never occur
297      */
298     public Window getWindow() throws RuntimeException {
299         return getWindow(this);
300     }
301 
302     /**
303      * Gets the window that is the top scope for the specified object.
304      * @param s the JavaScript object whose associated window is to be returned
305      * @return the window associated with the specified JavaScript object
306      * @throws RuntimeException if the window cannot be found, which should never occur
307      */
308     protected static Window getWindow(final Scriptable s) throws RuntimeException {
309         if (s instanceof Window window) {
310             return window;
311         }
312 
313         final TopLevel topLevel = ScriptableObject.getTopLevelScope(s.getParentScope());
314         if (topLevel.getGlobalThis() instanceof Window window) {
315             return window;
316         }
317         throw new RuntimeException("Unable to find window associated with " + s);
318     }
319 
320     protected static WindowOrWorkerGlobalScope getWindowOrWorkerGlobalScope(
321                         final Scriptable s) throws RuntimeException {
322         if (s instanceof WindowOrWorkerGlobalScope wow) {
323             return wow;
324         }
325 
326         final TopLevel topLevel = ScriptableObject.getTopLevelScope(s.getParentScope());
327         if (topLevel.getGlobalThis() instanceof WindowOrWorkerGlobalScope wow) {
328             return wow;
329         }
330         throw new RuntimeException("Unable to find WindowOrWorkerGlobalScope associated with " + s);
331     }
332 
333     /**
334      * Gets the browser version currently used.
335      * @return the browser version
336      */
337     public BrowserVersion getBrowserVersion() {
338         final DomNode node = getDomNodeOrNull();
339         if (node != null) {
340             return node.getPage().getWebClient().getBrowserVersion();
341         }
342 
343         final Window window = getWindow();
344         if (window != null) {
345             final WebWindow webWindow = window.getWebWindow();
346             if (webWindow != null) {
347                 return webWindow.getWebClient().getBrowserVersion();
348             }
349         }
350 
351         return null;
352     }
353 
354     /**
355      * {@inheritDoc}
356      */
357     @Override
358     public boolean hasInstance(final Scriptable instance) {
359         if (getPrototype() == null) {
360             // to handle cases like "x instanceof HTMLElement",
361             // but HTMLElement is not in the prototype chain of any element
362             final Object prototype = get("prototype", this);
363             if (!(prototype instanceof ScriptableObject)) {
364                 throw JavaScriptEngine.throwAsScriptRuntimeEx(new Exception("Null prototype"));
365             }
366             return ((ScriptableObject) prototype).hasInstance(instance);
367         }
368 
369         return super.hasInstance(instance);
370     }
371 
372     /**
373      * {@inheritDoc}
374      */
375     @Override
376     protected Object equivalentValues(Object value) {
377         if (value instanceof HtmlUnitScriptableProxy<?> proxy) {
378             value = proxy.getDelegee();
379         }
380         return super.equivalentValues(value);
381     }
382 
383     /**
384      * {@inheritDoc}
385      */
386     @Override
387     public HtmlUnitScriptable clone() {
388         try {
389             return (HtmlUnitScriptable) super.clone();
390         }
391         catch (final Exception e) {
392             throw new IllegalStateException("Clone not supported");
393         }
394     }
395 
396     protected NativePromise setupPromise(final FailableSupplier<Object, IOException> resolver) {
397         final VarScope scope = ScriptableObject.getTopLevelScope(getParentScope());
398         final JSFunction ctor = (JSFunction) getProperty(scope, "Promise");
399 
400         try {
401             final JSFunction resolve = (JSFunction) getProperty(ctor, "resolve");
402             return (NativePromise) resolve.call(Context.getCurrentContext(), scope,
403                                                 ctor, new Object[] {resolver.get()});
404         }
405         catch (final IOException e) {
406             final JSFunction reject = (JSFunction) getProperty(ctor, "reject");
407             return (NativePromise) reject.call(Context.getCurrentContext(), scope, ctor, new Object[] {e.getMessage()});
408         }
409     }
410 
411     protected NativePromise setupRejectedPromise(final Supplier<Object> resolver) {
412         final VarScope scope = ScriptableObject.getTopLevelScope(getParentScope());
413         final JSFunction ctor = (JSFunction) getProperty(scope, "Promise");
414         final JSFunction reject = (JSFunction) getProperty(ctor, "reject");
415         return (NativePromise) reject.call(Context.getCurrentContext(), scope, ctor, new Object[] {resolver.get()});
416     }
417 }