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