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