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.worker;
16  
17  import java.io.IOException;
18  import java.lang.reflect.Method;
19  import java.net.URL;
20  import java.util.HashMap;
21  import java.util.List;
22  import java.util.Map;
23  
24  import org.apache.commons.logging.Log;
25  import org.apache.commons.logging.LogFactory;
26  import org.htmlunit.BrowserVersion;
27  import org.htmlunit.WebClient;
28  import org.htmlunit.WebRequest;
29  import org.htmlunit.WebResponse;
30  import org.htmlunit.corejs.javascript.Context;
31  import org.htmlunit.corejs.javascript.ContextAction;
32  import org.htmlunit.corejs.javascript.ContextFactory;
33  import org.htmlunit.corejs.javascript.Function;
34  import org.htmlunit.corejs.javascript.FunctionObject;
35  import org.htmlunit.corejs.javascript.Scriptable;
36  import org.htmlunit.corejs.javascript.ScriptableObject;
37  import org.htmlunit.corejs.javascript.TopLevel;
38  import org.htmlunit.corejs.javascript.VarScope;
39  import org.htmlunit.html.HtmlPage;
40  import org.htmlunit.javascript.AbstractJavaScriptEngine;
41  import org.htmlunit.javascript.HtmlUnitContextFactory;
42  import org.htmlunit.javascript.HtmlUnitScriptable;
43  import org.htmlunit.javascript.JavaScriptEngine;
44  import org.htmlunit.javascript.background.BasicJavaScriptJob;
45  import org.htmlunit.javascript.background.JavaScriptJob;
46  import org.htmlunit.javascript.configuration.ClassConfiguration;
47  import org.htmlunit.javascript.configuration.JsxClass;
48  import org.htmlunit.javascript.configuration.JsxConstructor;
49  import org.htmlunit.javascript.configuration.JsxFunction;
50  import org.htmlunit.javascript.configuration.JsxGetter;
51  import org.htmlunit.javascript.configuration.JsxSetter;
52  import org.htmlunit.javascript.configuration.WorkerJavaScriptConfiguration;
53  import org.htmlunit.javascript.host.Window;
54  import org.htmlunit.javascript.host.WindowOrWorkerGlobalScopeMixin;
55  import org.htmlunit.javascript.host.event.Event;
56  import org.htmlunit.javascript.host.event.MessageEvent;
57  import org.htmlunit.javascript.host.event.SecurityPolicyViolationEvent;
58  import org.htmlunit.javascript.host.media.MediaSource;
59  import org.htmlunit.javascript.host.media.SourceBuffer;
60  import org.htmlunit.javascript.host.media.SourceBufferList;
61  import org.htmlunit.util.MimeType;
62  
63  /**
64   * The scope for the execution of {@link Worker}s.
65   *
66   * @author Marc Guillemot
67   * @author Ronald Brill
68   * @author Rural Hunter
69   */
70  @JsxClass
71  public class DedicatedWorkerGlobalScope extends WorkerGlobalScope {
72  
73      private static final Log LOG = LogFactory.getLog(DedicatedWorkerGlobalScope.class);
74  
75      private static final Method GETTER_NAME;
76      private static final Method SETTER_NAME;
77  
78      private Map<Class<? extends Scriptable>, Scriptable> prototypes_ = new HashMap<>();
79      private final Window owningWindow_;
80      private final String origin_;
81      private String name_;
82      private final Worker worker_;
83      private WorkerLocation workerLocation_;
84      private WorkerNavigator workerNavigator_;
85  
86      static {
87          try {
88              GETTER_NAME = DedicatedWorkerGlobalScope.class.getDeclaredMethod("jsGetName");
89              SETTER_NAME = DedicatedWorkerGlobalScope.class.getDeclaredMethod("jsSetName", Scriptable.class);
90          }
91          catch (NoSuchMethodException | SecurityException e) {
92              throw new RuntimeException(e);
93          }
94      }
95  
96      /**
97       * For prototype instantiation.
98       */
99      public DedicatedWorkerGlobalScope() {
100         // prototype constructor
101         super();
102         owningWindow_ = null;
103         origin_ = null;
104         name_ = null;
105         worker_ = null;
106         workerLocation_ = null;
107     }
108 
109     /**
110      * JavaScript constructor.
111      */
112     @Override
113     @JsxConstructor
114     public void jsConstructor() {
115         // nothing to do
116     }
117 
118     /**
119      * Constructor.
120      * @param webClient the WebClient
121      * @param worker the started worker
122      * @throws Exception in case of problem
123      */
124     DedicatedWorkerGlobalScope(final Window owningWindow, final Context context, final WebClient webClient,
125             final String name, final Worker worker) throws Exception {
126         super();
127 
128         final BrowserVersion browserVersion = webClient.getBrowserVersion();
129 
130         final TopLevel scope = context.initSafeStandardObjects(new TopLevel(this));
131         this.setParentScope(scope);
132 
133         JavaScriptEngine.configureRhino(webClient, browserVersion, scope, this);
134 
135         final WorkerJavaScriptConfiguration jsConfig = WorkerJavaScriptConfiguration.getInstance(browserVersion);
136 
137         final ClassConfiguration config = jsConfig.getDedicatedWorkerGlobalScopeClassConfiguration();
138         final HtmlUnitScriptable prototype = JavaScriptEngine.configureClass(config, scope);
139         setPrototype(prototype);
140 
141         final Map<Class<? extends Scriptable>, Scriptable> prototypes = new HashMap<>();
142         final Map<String, Scriptable> prototypesPerJSName = new HashMap<>();
143 
144         prototypes.put(config.getHostClass(), prototype);
145         prototypesPerJSName.put(config.getClassName(), prototype);
146 
147         final FunctionObject functionObject =
148                 new FunctionObject(DedicatedWorkerGlobalScope.class.getSimpleName(),
149                         config.getJsConstructor().getValue(), scope);
150         functionObject.addAsConstructor(scope, prototype, ScriptableObject.DONTENUM);
151 
152         JavaScriptEngine.configureGlobalThis(scope, this, config, functionObject, jsConfig,
153                 browserVersion, prototypes, prototypesPerJSName);
154         // remove some aliases
155         delete("webkitURL");
156         delete("WebKitCSSMatrix");
157 
158         // hack for the moment
159         if (browserVersion.isFirefox()) {
160             delete(MediaSource.class.getSimpleName());
161             delete(SecurityPolicyViolationEvent.class.getSimpleName());
162             delete(SourceBuffer.class.getSimpleName());
163             delete(SourceBufferList.class.getSimpleName());
164         }
165 
166         if (!webClient.getOptions().isWebSocketEnabled()) {
167             delete("WebSocket");
168         }
169 
170         setPrototypes(prototypes);
171 
172         owningWindow_ = owningWindow;
173         final URL currentURL = owningWindow.getWebWindow().getEnclosedPage().getUrl();
174         origin_ = currentURL.getProtocol() + "://" + currentURL.getHost() + ':' + currentURL.getPort();
175 
176         name_ = name;
177         defineProperty(scope, "name", null, GETTER_NAME, SETTER_NAME, ScriptableObject.READONLY);
178 
179         worker_ = worker;
180         workerLocation_ = null;
181     }
182 
183     /**
184      * Get the scope itself.
185      * @return this
186      */
187     @JsxGetter
188     public Object getSelf() {
189         return this;
190     }
191 
192     /**
193      * Returns the {@code onmessage} event handler.
194      * @return the {@code onmessage} event handler
195      */
196     @JsxGetter
197     public Function getOnmessage() {
198         return getEventHandler(Event.TYPE_MESSAGE);
199     }
200 
201     /**
202      * Sets the {@code onmessage} event handler.
203      * @param onmessage the {@code onmessage} event handler
204      */
205     @JsxSetter
206     public void setOnmessage(final Object onmessage) {
207         setEventHandler(Event.TYPE_MESSAGE, onmessage);
208     }
209 
210     /**
211      * @return returns the WorkerLocation associated with the worker
212      */
213     @JsxGetter
214     public WorkerLocation getLocation() {
215         return workerLocation_;
216     }
217 
218     /**
219      * @return returns the WorkerNavigator associated with the worker
220      */
221     @JsxGetter
222     public WorkerNavigator getNavigator() {
223         return workerNavigator_;
224     }
225 
226     /**
227      * @return the {@code name}
228      */
229     public String jsGetName() {
230         return name_;
231     }
232 
233     /**
234      * Sets the {@code name}.
235      * @param name the new name
236      */
237     public void jsSetName(final Scriptable name) {
238         name_ = JavaScriptEngine.toString(name);
239     }
240 
241     /**
242      * Posts a message to the {@link Worker} in the page's context.
243      * @param message the message
244      */
245     @JsxFunction
246     public void postMessage(final Object message) {
247         final MessageEvent event = new MessageEvent();
248         event.initMessageEvent(Event.TYPE_MESSAGE, false, false, message, origin_, "",
249                                     owningWindow_, JavaScriptEngine.UNDEFINED);
250         event.setParentScope(getTopLevelScope(getParentScope()));
251         event.setPrototype(owningWindow_.getPrototype(event.getClass()));
252 
253         if (LOG.isDebugEnabled()) {
254             LOG.debug("[DedicatedWorker] postMessage: {}" + message);
255         }
256         final JavaScriptEngine jsEngine =
257                 (JavaScriptEngine) owningWindow_.getWebWindow().getWebClient().getJavaScriptEngine();
258         final ContextAction<Object> action = cx -> {
259             worker_.getEventListenersContainer().executeCapturingListeners(event, null);
260             final Object[] args = {event};
261             worker_.getEventListenersContainer().executeBubblingListeners(event, args);
262             return null;
263         };
264 
265         final HtmlUnitContextFactory cf = jsEngine.getContextFactory();
266 
267         final JavaScriptJob job = new WorkerJob(cf, action, "postMessage: " + JavaScriptEngine.toString(message));
268 
269         final HtmlPage page = (HtmlPage) owningWindow_.getDocument().getPage();
270         owningWindow_.getWebWindow().getJobManager().addJob(job, page);
271     }
272 
273     void messagePosted(final Object message) {
274         final MessageEvent event = new MessageEvent();
275         event.initMessageEvent(Event.TYPE_MESSAGE, false, false, message, origin_, "",
276                                     owningWindow_, JavaScriptEngine.UNDEFINED);
277         event.setParentScope(getTopLevelScope(getParentScope()));
278         event.setPrototype(owningWindow_.getPrototype(event.getClass()));
279 
280         final JavaScriptEngine jsEngine =
281                 (JavaScriptEngine) owningWindow_.getWebWindow().getWebClient().getJavaScriptEngine();
282         final ContextAction<Object> action = cx -> {
283             executeEvent(cx, event);
284             return null;
285         };
286 
287         final HtmlUnitContextFactory cf = jsEngine.getContextFactory();
288 
289         final JavaScriptJob job = new WorkerJob(cf, action, "messagePosted: " + JavaScriptEngine.toString(message));
290 
291         final HtmlPage page = (HtmlPage) owningWindow_.getDocument().getPage();
292         owningWindow_.getWebWindow().getJobManager().addJob(job, page);
293     }
294 
295     void executeEvent(final Context cx, final MessageEvent event) {
296         final List<Scriptable> handlers = getEventListenersContainer().getListeners(Event.TYPE_MESSAGE, false);
297         if (handlers != null) {
298             final Object[] args = {event};
299             for (final Scriptable scriptable : handlers) {
300                 if (scriptable instanceof Function handlerFunction) {
301                     handlerFunction.call(cx, ScriptableObject.getTopLevelScope(event.getParentScope()), this, args);
302                 }
303             }
304         }
305 
306         final Function handlerFunction = getEventHandler(Event.TYPE_MESSAGE);
307         if (handlerFunction != null) {
308             final Object[] args = {event};
309             handlerFunction.call(cx, getParentScope(), this, args);
310         }
311     }
312 
313     /**
314      * Import external script(s).
315      * @param cx the current context
316      * @param scope the scope
317      * @param thisObj this object
318      * @param args the script(s) to import
319      * @param funObj the JS function called
320      * @throws IOException in case of problem loading/executing the scripts
321      */
322     @JsxFunction
323     public static void importScripts(final Context cx, final VarScope scope,
324             final Scriptable thisObj, final Object[] args, final Function funObj) throws IOException {
325         final DedicatedWorkerGlobalScope workerScope = (DedicatedWorkerGlobalScope) thisObj;
326 
327         final WebClient webClient = workerScope.owningWindow_.getWebWindow().getWebClient();
328         for (final Object arg : args) {
329             final String url = JavaScriptEngine.toString(arg);
330             workerScope.loadAndExecute(webClient, url, cx, true);
331         }
332     }
333 
334     void loadAndExecute(final WebClient webClient, final String url,
335             final Context context, final boolean checkMimeType) throws IOException {
336         final HtmlPage page = (HtmlPage) owningWindow_.getDocument().getPage();
337         final URL fullUrl = page.getFullyQualifiedUrl(url);
338 
339         workerLocation_ = new WorkerLocation(fullUrl, origin_);
340         workerLocation_.setParentScope(getParentScope());
341         workerLocation_.setPrototype(getPrototype(workerLocation_.getClass()));
342 
343         workerNavigator_ = new WorkerNavigator(webClient.getBrowserVersion());
344         workerNavigator_.setParentScope(getParentScope());
345         workerNavigator_.setPrototype(getPrototype(workerNavigator_.getClass()));
346 
347         final WebRequest webRequest = new WebRequest(fullUrl);
348         final WebResponse response = webClient.loadWebResponse(webRequest);
349         if (checkMimeType && !MimeType.isJavascriptMimeType(response.getContentType())) {
350             throw JavaScriptEngine.reportRuntimeError(
351                     "NetworkError: importScripts response is not a javascript response");
352         }
353 
354         final String scriptCode = response.getContentAsString();
355         final AbstractJavaScriptEngine<?> javaScriptEngine = webClient.getJavaScriptEngine();
356 
357         final ContextAction<Object> action =
358                 cx -> javaScriptEngine.execute(page, getParentScope(), scriptCode, fullUrl.toExternalForm(), 1);
359 
360         final HtmlUnitContextFactory cf = javaScriptEngine.getContextFactory();
361 
362         if (context != null) {
363             action.run(context);
364         }
365         else {
366             final JavaScriptJob job = new WorkerJob(cf, action, "loadAndExecute " + url);
367             owningWindow_.getWebWindow().getJobManager().addJob(job, page);
368         }
369     }
370 
371     /**
372      * Sets a chunk of JavaScript to be invoked at some specified time later.
373      * The invocation occurs only if the window is opened after the delay
374      * and does not contain another page than the one that originated the setTimeout.
375      *
376      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout">
377      *     MDN web docs</a>
378      *
379      * @param context the JavaScript context
380      * @param scope the scope
381      * @param thisObj the scriptable
382      * @param args the arguments passed into the method
383      * @param function the function
384      * @return the id of the created timer
385      */
386     @JsxFunction
387     public static Object setTimeout(final Context context, final VarScope scope,
388             final Scriptable thisObj, final Object[] args, final Function function) {
389         return WindowOrWorkerGlobalScopeMixin.setTimeout(context,
390                 ((DedicatedWorkerGlobalScope) thisObj).owningWindow_, args, function);
391     }
392 
393     /**
394      * Sets a chunk of JavaScript to be invoked each time a specified number of milliseconds has elapsed.
395      *
396      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval">
397      *     MDN web docs</a>
398      * @param context the JavaScript context
399      * @param scope the scope
400      * @param thisObj the scriptable
401      * @param args the arguments passed into the method
402      * @param function the function
403      * @return the id of the created interval
404      */
405     @JsxFunction
406     public static Object setInterval(final Context context, final VarScope scope,
407             final Scriptable thisObj, final Object[] args, final Function function) {
408         return WindowOrWorkerGlobalScopeMixin.setInterval(context,
409                 ((DedicatedWorkerGlobalScope) thisObj).owningWindow_, args, function);
410     }
411 
412     /**
413      * Returns the prototype object corresponding to the specified HtmlUnit class inside the window scope.
414      * @param jsClass the class whose prototype is to be returned
415      * @return the prototype object corresponding to the specified class inside the specified scope
416      */
417     @Override
418     public Scriptable getPrototype(final Class<? extends HtmlUnitScriptable> jsClass) {
419         return prototypes_.get(jsClass);
420     }
421 
422     /**
423      * Sets the prototypes for HtmlUnit host classes.
424      * @param map a Map of ({@link Class}, {@link Scriptable})
425      */
426     public void setPrototypes(final Map<Class<? extends Scriptable>, Scriptable> map) {
427         prototypes_ = map;
428     }
429 }
430 
431 class WorkerJob extends BasicJavaScriptJob {
432     private final ContextFactory contextFactory_;
433     private final ContextAction<Object> action_;
434     private final String description_;
435 
436     WorkerJob(final ContextFactory contextFactory, final ContextAction<Object> action, final String description) {
437         super();
438         contextFactory_ = contextFactory;
439         action_ = action;
440         description_ = description;
441     }
442 
443     @Override
444     public void run() {
445         contextFactory_.call(action_);
446     }
447 
448     @Override
449     public String toString() {
450         return "WorkerJob(" + description_ + ")";
451     }
452 }