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.host;
16  
17  import java.nio.charset.StandardCharsets;
18  import java.util.Arrays;
19  import java.util.Base64;
20  
21  import org.htmlunit.Page;
22  import org.htmlunit.WebWindow;
23  import org.htmlunit.corejs.javascript.Context;
24  import org.htmlunit.corejs.javascript.Function;
25  import org.htmlunit.corejs.javascript.FunctionObject;
26  import org.htmlunit.corejs.javascript.Scriptable;
27  import org.htmlunit.corejs.javascript.ScriptableObject;
28  import org.htmlunit.corejs.javascript.VarScope;
29  import org.htmlunit.javascript.HtmlUnitScriptable;
30  import org.htmlunit.javascript.JavaScriptEngine;
31  import org.htmlunit.javascript.background.BackgroundJavaScriptFactory;
32  import org.htmlunit.javascript.background.JavaScriptJob;
33  import org.htmlunit.util.StringUtils;
34  
35  /**
36   * The implementation of {@code WindowOrWorkerGlobalScope}
37   * to be used by the implementers of the mixin.
38   *
39   * @author Ronald Brill
40   * @author Rural Hunter
41   * @author Lai Quang Duong
42   */
43  public final class WindowOrWorkerGlobalScopeMixin {
44  
45      /**
46       * The minimum delay that can be used with setInterval() or setTimeout(). Browser minimums are
47       * usually in the 10ms to 15ms range, but there's really no reason for us to waste that much time.
48       * <a href="http://jsninja.com/Timers#Minimum_Timer_Delay_and_Reliability">
49       * http://jsninja.com/Timers#Minimum_Timer_Delay_and_Reliability</a>
50       */
51      private static final int MIN_TIMER_DELAY = 1;
52  
53      private WindowOrWorkerGlobalScopeMixin() {
54          super();
55      }
56  
57      /**
58       * Decodes a string of data which has been encoded using base-64 encoding.
59       * @param encodedData the encoded string
60       * @param scriptable the HtmlUnitScriptable
61       * @return the decoded value
62       */
63      public static String atob(final String encodedData, final HtmlUnitScriptable scriptable) {
64          final String withoutWhitespace = StringUtils.replaceChars(encodedData, " \t\r\n\u000c", "");
65          final byte[] bytes = withoutWhitespace.getBytes(StandardCharsets.ISO_8859_1);
66          try {
67              return new String(Base64.getDecoder().decode(bytes), StandardCharsets.ISO_8859_1);
68          }
69          catch (final IllegalArgumentException e) {
70              throw JavaScriptEngine.asJavaScriptException(
71                      scriptable,
72                      "Failed to execute atob(): " + e.getMessage(),
73                      org.htmlunit.javascript.host.dom.DOMException.INVALID_CHARACTER_ERR);
74          }
75      }
76  
77      /**
78       * Creates a base-64 encoded ASCII string from a string of binary data.
79       * @param stringToEncode string to encode
80       * @param scriptable the HtmlUnitScriptable
81       * @return the encoded string
82       */
83      public static String btoa(final String stringToEncode, final HtmlUnitScriptable scriptable) {
84          final int l = stringToEncode.length();
85          for (int i = 0; i < l; i++) {
86              if (stringToEncode.charAt(i) > 255) {
87                  throw JavaScriptEngine.asJavaScriptException(
88                          scriptable,
89                          "Function btoa supports only latin1 characters",
90                          org.htmlunit.javascript.host.dom.DOMException.INVALID_CHARACTER_ERR);
91              }
92          }
93  
94          final byte[] bytes = stringToEncode.getBytes(StandardCharsets.ISO_8859_1);
95          try {
96              return new String(Base64.getEncoder().encode(bytes), StandardCharsets.UTF_8);
97          }
98          catch (final IllegalArgumentException e) {
99              throw JavaScriptEngine.asJavaScriptException(
100                     scriptable,
101                     "Failed to execute btoa(): " + e.getMessage(),
102                     org.htmlunit.javascript.host.dom.DOMException.INVALID_CHARACTER_ERR);
103         }
104     }
105 
106     /**
107      * Sets a chunk of JavaScript to be invoked at some specified time later.
108      * The invocation occurs only if the window is opened after the delay
109      * and does not contain an other page than the one that originated the setTimeout.
110      *
111      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout">
112      *     MDN web docs</a>
113      *
114      * @param context the JavaScript context
115      * @param thisObj the scriptable
116      * @param args the arguments passed into the method
117      * @param function the function
118      * @return the id of the created timer
119      */
120     public static Object setTimeout(final Context context, final Scriptable thisObj,
121             final Object[] args, final Function function) {
122         if (args.length < 1) {
123             throw JavaScriptEngine.typeError("Function not provided");
124         }
125 
126         final int timeout = JavaScriptEngine.toInt32((args.length > 1) ? args[1] : JavaScriptEngine.UNDEFINED);
127         final Object[] params = (args.length > 2)
128                 ? Arrays.copyOfRange(args, 2, args.length)
129                 : JavaScriptEngine.EMPTY_ARGS;
130         return setTimeoutIntervalImpl((Window) thisObj, args[0], timeout, true, params);
131     }
132 
133     /**
134      * Sets a chunk of JavaScript to be invoked each time a specified number of milliseconds has elapsed.
135      *
136      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval">
137      *     MDN web docs</a>
138      * @param context the JavaScript context
139      * @param thisObj the scriptable
140      * @param args the arguments passed into the method
141      * @param function the function
142      * @return the id of the created interval
143      */
144     public static Object setInterval(final Context context, final Scriptable thisObj,
145             final Object[] args, final Function function) {
146         if (args.length < 1) {
147             throw JavaScriptEngine.typeError("Function not provided");
148         }
149 
150         final int timeout = JavaScriptEngine.toInt32((args.length > 1) ? args[1] : JavaScriptEngine.UNDEFINED);
151         final Object[] params = (args.length > 2)
152                 ? Arrays.copyOfRange(args, 2, args.length)
153                 : JavaScriptEngine.EMPTY_ARGS;
154         return setTimeoutIntervalImpl((Window) thisObj, args[0], timeout, false, params);
155     }
156 
157     /**
158      * Queues a microtask to be executed.
159      *
160      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask">
161      *     MDN web docs</a>
162      * @param thisObj the scriptable
163      * @param args the arguments passed into the method
164      * @return undefined
165      */
166     public static Object queueMicrotask(final Scriptable thisObj, final Object[] args) {
167         if (args.length < 1) {
168             throw JavaScriptEngine.typeError("At least 1 argument required");
169         }
170         if (!(args[0] instanceof Function)) {
171             throw JavaScriptEngine.typeError("Argument 1 is not callable");
172         }
173 
174         final Function callback = (Function) args[0];
175         final VarScope scope = ScriptableObject.getTopLevelScope(thisObj.getParentScope());
176         final Context cx = Context.getCurrentContext();
177         cx.enqueueMicrotask(() -> {
178             try {
179                 callback.call(cx, scope, thisObj, JavaScriptEngine.EMPTY_ARGS);
180             }
181             catch (final Exception e) {
182                 // uncaught exception in a microtask must not prevent remaining microtasks from running.
183             }
184         });
185 
186         return JavaScriptEngine.UNDEFINED;
187     }
188 
189     private static int setTimeoutIntervalImpl(final Window window, final Object code,
190             int timeout, final boolean isTimeout, final Object[] params) {
191         if (timeout < MIN_TIMER_DELAY) {
192             timeout = MIN_TIMER_DELAY;
193         }
194 
195         final WebWindow webWindow = window.getWebWindow();
196         final Page page = (Page) window.getDomNodeOrNull();
197         Integer period = null;
198         if (!isTimeout) {
199             period = timeout;
200         }
201 
202         if (code instanceof String s) {
203             final String description = "window.set"
204                                         + (isTimeout ? "Timeout" : "Interval")
205                                         + "(" + s + ", " + timeout + ")";
206             final JavaScriptJob job = BackgroundJavaScriptFactory.theFactory().
207                     createJavaScriptJob(timeout, period, description, webWindow, s);
208             return webWindow.getJobManager().addJob(job, page);
209         }
210 
211         if (code instanceof Function f) {
212             final String functionName;
213             if (f instanceof FunctionObject object) {
214                 functionName = object.getFunctionName();
215             }
216             else {
217                 functionName = String.valueOf(f); // can this happen?
218             }
219 
220             final String description = "window.set"
221                                         + (isTimeout ? "Timeout" : "Interval")
222                                         + "(" + functionName + ", " + timeout + ")";
223             final JavaScriptJob job = BackgroundJavaScriptFactory.theFactory().
224                     createJavaScriptJob(timeout, period, description, webWindow, f, params);
225             return webWindow.getJobManager().addJob(job, page);
226         }
227 
228         throw JavaScriptEngine.reportRuntimeError("Unknown type for function.");
229     }
230 }