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;
16  
17  import java.util.Set;
18  
19  import org.htmlunit.Page;
20  import org.htmlunit.WebWindow;
21  import org.htmlunit.corejs.javascript.Context;
22  import org.htmlunit.corejs.javascript.Function;
23  import org.htmlunit.corejs.javascript.FunctionObject;
24  import org.htmlunit.corejs.javascript.Scriptable;
25  import org.htmlunit.corejs.javascript.TopLevel;
26  import org.htmlunit.corejs.javascript.VarScope;
27  import org.htmlunit.javascript.AbstractJavaScriptEngine;
28  import org.htmlunit.javascript.HtmlUnitContextFactory;
29  import org.htmlunit.javascript.JavaScriptEngine;
30  import org.htmlunit.javascript.PostponedAction;
31  import org.htmlunit.javascript.configuration.JsxClass;
32  import org.htmlunit.javascript.configuration.JsxConstructor;
33  import org.htmlunit.javascript.configuration.JsxFunction;
34  import org.htmlunit.javascript.configuration.JsxGetter;
35  import org.htmlunit.javascript.configuration.JsxSetter;
36  import org.htmlunit.javascript.host.event.Event;
37  import org.htmlunit.javascript.host.event.EventTarget;
38  import org.htmlunit.javascript.host.event.MessageEvent;
39  import org.htmlunit.util.UrlUtils;
40  
41  /**
42   * A JavaScript object for {@code BroadcastChannel}.
43   *
44   * @author Ronald Brill
45   */
46  @JsxClass
47  public class BroadcastChannel extends EventTarget {
48  
49      private String name_;
50  
51      /**
52       * JavaScript constructor.
53       * @param cx the current context
54       * @param scope the scope
55       * @param args the arguments to the WebSocket constructor
56       * @param ctorObj the function object
57       * @param inNewExpr Is new or not
58       * @return the java object to allow JavaScript to access
59       */
60      @JsxConstructor
61      public static BroadcastChannel jsConstructor(final Context cx, final VarScope scope,
62              final Object[] args, final Function ctorObj, final boolean inNewExpr) {
63          if (args.length < 1 || JavaScriptEngine.isUndefined(args[0])) {
64              throw JavaScriptEngine.typeError("BroadcastChannel constructor requires a channel name argument");
65          }
66  
67          final BroadcastChannel broadcastChannel = new BroadcastChannel();
68          final Window window = getWindow(ctorObj);
69          broadcastChannel.setParentScope(getTopLevelScope(scope));
70          broadcastChannel.setPrototype(((FunctionObject) ctorObj).getClassPrototype());
71  
72          broadcastChannel.name_ = JavaScriptEngine.toString(args[0]);
73  
74          final Set<BroadcastChannel> broadcastChannels = window.getWebWindow().getWebClient().getBroadcastChannels();
75          synchronized (broadcastChannels) {
76              broadcastChannels.add(broadcastChannel);
77          }
78  
79          return broadcastChannel;
80      }
81  
82      /**
83       * Returns the channel name.
84       * @return the channel name
85       */
86      @JsxGetter
87      public String getName() {
88          return name_;
89      }
90  
91      /**
92       * Returns the value of the {@code onmessage} property.
93       * @return the value of the {@code onmessage} property
94       */
95      @JsxGetter
96      public Function getOnmessage() {
97          return getEventListenersContainer().getEventHandler(Event.TYPE_MESSAGE);
98      }
99  
100     /**
101      * Sets the value of the {@code onmessage} property.
102      * @param onmessage the value of the {@code onmessage} property
103      */
104     @JsxSetter
105     public void setOnmessage(final Object onmessage) {
106         getEventListenersContainer().setEventHandler(Event.TYPE_MESSAGE, onmessage);
107     }
108 
109     /**
110      * Returns the value of the {@code onmessageerror} property.
111      * @return the value of the {@code onmessageerror} property
112      */
113     @JsxGetter
114     public Function getOnmessageerror() {
115         return getEventListenersContainer().getEventHandler("messageerror");
116     }
117 
118     /**
119      * Sets the value of the {@code onmessageerror} property.
120      * @param onmessageerror the value of the {@code onmessageerror} property
121      */
122     @JsxSetter
123     public void setOnmessageerror(final Object onmessageerror) {
124         getEventListenersContainer().setEventHandler("messageerror", onmessageerror);
125     }
126 
127     /**
128      * Posts a message to all other BroadcastChannel objects with the same name.
129      * @param message the message to send
130      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel/postMessage">MDN documentation</a>
131      */
132     @JsxFunction
133     public void postMessage(final Object message) {
134         if (name_ == null) {
135             return;
136         }
137 
138         final Window w = getWindow();
139         final WebWindow webWindow = w.getWebWindow();
140         final Page page = webWindow.getEnclosedPage();
141         final java.net.URL currentURL = page.getUrl();
142         final String origin = currentURL.getProtocol() + "://" + currentURL.getHost() + ':' + currentURL.getPort();
143 
144         final AbstractJavaScriptEngine<?> jsEngine = webWindow.getWebClient().getJavaScriptEngine();
145 
146         final Set<BroadcastChannel> broadcastChannels = webWindow.getWebClient().getBroadcastChannels();
147 
148         synchronized (broadcastChannels) {
149             for (final BroadcastChannel channel : broadcastChannels) {
150                 if (channel != this && name_.equals(channel.name_)) {
151                     final Window channelWindow = channel.getWindow();
152                     final Page channelPage = channelWindow.getWebWindow().getEnclosedPage();
153 
154                     if (UrlUtils.isSameOrigin(currentURL, channelPage.getUrl())) {
155                         final TopLevel scope = getTopLevelScope(channelWindow.getParentScope());
156                         final Scriptable ports = JavaScriptEngine.newArray(scope, 0);
157 
158                         final MessageEvent event = new MessageEvent();
159                         event.initMessageEvent(Event.TYPE_MESSAGE, false, false, message, origin, "", null, ports);
160                         event.setParentScope(scope);
161                         event.setPrototype(channelWindow.getPrototype(event.getClass()));
162 
163                         final PostponedAction action =
164                                 new PostponedAction(channelPage, "BroadcastChannel.postMessage") {
165                                 @Override
166                                 public void execute() {
167                                     final HtmlUnitContextFactory cf = jsEngine.getContextFactory();
168                                     cf.call(cx -> channel.dispatchEvent(event));
169                                 }
170                             };
171                         jsEngine.addPostponedAction(action);
172                     }
173                 }
174             }
175         }
176     }
177 
178     /**
179      * Closes the BroadcastChannel object, indicating it won't get any new messages,
180      * and allowing it to be garbage collected.
181      */
182     @JsxFunction
183     public void close() {
184         final Set<BroadcastChannel> broadcastChannels =
185                 getWindow().getWebWindow().getWebClient().getBroadcastChannels();
186         synchronized (broadcastChannels) {
187             broadcastChannels.remove(this);
188         }
189         name_ = null;
190     }
191 }