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