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