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