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