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