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