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