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