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 static java.nio.charset.StandardCharsets.UTF_8;
18  import static org.htmlunit.BrowserVersionFeatures.JS_NAVIGATOR_DO_NOT_TRACK_UNSPECIFIED;
19  import static org.htmlunit.javascript.configuration.SupportedBrowser.CHROME;
20  import static org.htmlunit.javascript.configuration.SupportedBrowser.EDGE;
21  import static org.htmlunit.javascript.configuration.SupportedBrowser.FF;
22  import static org.htmlunit.javascript.configuration.SupportedBrowser.FF_ESR;
23  
24  import java.io.IOException;
25  import java.net.MalformedURLException;
26  import java.net.URL;
27  import java.util.ArrayList;
28  
29  import org.apache.commons.logging.Log;
30  import org.apache.commons.logging.LogFactory;
31  import org.htmlunit.FormEncodingType;
32  import org.htmlunit.HttpHeader;
33  import org.htmlunit.HttpMethod;
34  import org.htmlunit.WebClient;
35  import org.htmlunit.WebRequest;
36  import org.htmlunit.WebRequest.HttpHint;
37  import org.htmlunit.WebWindow;
38  import org.htmlunit.corejs.javascript.Scriptable;
39  import org.htmlunit.corejs.javascript.typedarrays.NativeArrayBufferView;
40  import org.htmlunit.html.HtmlPage;
41  import org.htmlunit.javascript.HtmlUnitScriptable;
42  import org.htmlunit.javascript.JavaScriptEngine;
43  import org.htmlunit.javascript.configuration.JsxClass;
44  import org.htmlunit.javascript.configuration.JsxConstructor;
45  import org.htmlunit.javascript.configuration.JsxFunction;
46  import org.htmlunit.javascript.configuration.JsxGetter;
47  import org.htmlunit.javascript.host.file.Blob;
48  import org.htmlunit.javascript.host.geo.Geolocation;
49  import org.htmlunit.javascript.host.media.MediaDevices;
50  import org.htmlunit.javascript.host.network.NetworkInformation;
51  // TODO verify actual package - not visible from XMLHttpRequest.java's import list,
52  // which suggests it may already share a package with XMLHttpRequest rather than
53  // living here; adjust this import (and the FormData branch below) accordingly.
54  import org.htmlunit.javascript.host.xml.FormData;
55  import org.htmlunit.util.StringUtils;
56  import org.htmlunit.util.UrlUtils;
57  
58  /**
59   * JavaScript host object for {@code Navigator}.
60   *
61   * @author Mike Bowler
62   * @author Daniel Gredler
63   * @author Chris Erskine
64   * @author Ahmed Ashour
65   * @author Marc Guillemot
66   * @author Frank Danek
67   * @author Ronald Brill
68   *
69   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator">MDN Documentation</a>
70   */
71  @JsxClass
72  public class Navigator extends HtmlUnitScriptable {
73  
74      private static final Log LOG = LogFactory.getLog(Navigator.class);
75  
76      private PluginArray plugins_;
77      private MimeTypeArray mimeTypes_;
78      private MediaDevices mediaDevices_;
79  
80      /**
81       * Creates an instance of this object.
82       */
83      @JsxConstructor
84      public void jsConstructor() {
85          // nothing to do
86      }
87  
88      /**
89       * Returns the {@code appCodeName} property.
90       *
91       * @return the {@code appCodeName} property
92       */
93      @JsxGetter
94      public String getAppCodeName() {
95          return getBrowserVersion().getApplicationCodeName();
96      }
97  
98      /**
99       * Returns the {@code appName} property.
100      *
101      * @return the {@code appName} property
102      */
103     @JsxGetter
104     public String getAppName() {
105         return getBrowserVersion().getApplicationName();
106     }
107 
108     /**
109      * Returns the {@code appVersion} property.
110      *
111      * @return the {@code appVersion} property
112      */
113     @JsxGetter
114     public String getAppVersion() {
115         return getBrowserVersion().getApplicationVersion();
116     }
117 
118     /**
119      * Returns the language of the browser.
120      *
121      * @return the browser language
122      */
123     @JsxGetter
124     public String getLanguage() {
125         return getBrowserVersion().getBrowserLanguage();
126     }
127 
128     /**
129      * Returns the preferred languages of the browser as an array.
130      *
131      * @return the languages array
132      */
133     @JsxGetter
134     public Scriptable getLanguages() {
135         final String acceptLang = getBrowserVersion().getAcceptLanguageHeader();
136         if (StringUtils.isEmptyOrNull(acceptLang)) {
137             return JavaScriptEngine.newArray(getParentScope(), 0);
138         }
139 
140         final ArrayList<String> res = new ArrayList<>();
141         final String[] parts = StringUtils.splitAtComma(acceptLang);
142         for (final String part : parts) {
143             if (!StringUtils.isEmptyOrNull(part)) {
144                 final String lang = StringUtils.substringBefore(part, ";").trim();
145                 if (!StringUtils.isEmptyOrNull(part)) {
146                     res.add(lang);
147                 }
148             }
149         }
150 
151         return JavaScriptEngine.newArray(getParentScope(), res.toArray());
152     }
153 
154     /**
155      * Returns the {@code cookieEnabled} property.
156      *
157      * @return the {@code cookieEnabled} property
158      */
159     @JsxGetter
160     public boolean isCookieEnabled() {
161         return getWindow().getWebWindow().getWebClient().getCookieManager().isCookiesEnabled();
162     }
163 
164     /**
165      * Returns the {@code onLine} property.
166      *
167      * @return the {@code onLine} property
168      */
169     @JsxGetter
170     public boolean isOnLine() {
171         return getBrowserVersion().isOnLine();
172     }
173 
174     /**
175      * Returns the {@code platform} property.
176      *
177      * @return the {@code platform} property
178      */
179     @JsxGetter
180     public String getPlatform() {
181         return getBrowserVersion().getPlatform();
182     }
183 
184     /**
185      * Returns the {@code product} property.
186      *
187      * @return the {@code product} property
188      */
189     @JsxGetter
190     public String getProduct() {
191         return "Gecko";
192     }
193 
194     /**
195      * Returns the build number of the current browser.
196      *
197      * @return the {@code productSub} property
198      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator/productSub">MDN Documentation</a>
199      */
200     @JsxGetter
201     public String getProductSub() {
202         return getBrowserVersion().getProductSub();
203     }
204 
205     /**
206      * Returns the {@code userAgent} property.
207      *
208      * @return the {@code userAgent} property
209      */
210     @JsxGetter
211     public String getUserAgent() {
212         return getBrowserVersion().getUserAgent();
213     }
214 
215     /**
216      * Returns the list of browser plugins.
217      *
218      * @return the {@code plugins} array
219      */
220     @JsxGetter
221     public PluginArray getPlugins() {
222         initPluginsAndMimeTypes();
223         return plugins_;
224     }
225 
226     private void initPluginsAndMimeTypes() {
227         // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/plugins
228         // Recent versions of the specification hard-code the returned list.
229         // If inline viewing of PDF files is supported the property lists five standard plugins.
230         // If inline PDF viewing is not supported then an empty list is returned.
231         if (plugins_ != null) {
232             return;
233         }
234         plugins_ = new PluginArray();
235         plugins_.setParentScope(getParentScope());
236         plugins_.setPrototype(getPrototype(PluginArray.class));
237 
238         Plugin plugin = new Plugin("PDF Viewer", "Portable Document Format", "internal-pdf-viewer");
239         plugin.setParentScope(getParentScope());
240         plugin.setPrototype(getPrototype(Plugin.class));
241 
242         // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/mimeTypes
243         // Recent versions of the specification hard-code the returned set of MIME types.
244         // If PDF files can be displayed inline then application/pdf and text/pdf are listed,
245         // otherwise an empty list is returned.
246         mimeTypes_ = new MimeTypeArray();
247         mimeTypes_.setParentScope(getParentScope());
248         mimeTypes_.setPrototype(getPrototype(MimeTypeArray.class));
249 
250         final MimeType mimeTypeAppPdf = new MimeType("application/pdf", "Portable Document Format", "pdf", plugin);
251         mimeTypeAppPdf.setParentScope(getParentScope());
252         mimeTypeAppPdf.setPrototype(getPrototype(MimeType.class));
253         mimeTypes_.add(mimeTypeAppPdf);
254 
255         final MimeType mimeTypeTxtPdf = new MimeType("text/pdf", "Portable Document Format", "pdf", plugin);
256         mimeTypeTxtPdf.setParentScope(getParentScope());
257         mimeTypeTxtPdf.setPrototype(getPrototype(MimeType.class));
258         mimeTypes_.add(mimeTypeTxtPdf);
259 
260         plugin.add(mimeTypeAppPdf);
261         plugin.add(mimeTypeTxtPdf);
262         plugins_.add(plugin);
263 
264         // all the others
265         plugin = new Plugin("Chrome PDF Viewer", "Portable Document Format", "internal-pdf-viewer");
266         plugin.setParentScope(getParentScope());
267         plugin.setPrototype(getPrototype(Plugin.class));
268         plugin.add(mimeTypeAppPdf);
269         plugin.add(mimeTypeTxtPdf);
270         plugins_.add(plugin);
271 
272         plugin = new Plugin("Chromium PDF Viewer", "Portable Document Format", "internal-pdf-viewer");
273         plugin.setParentScope(getParentScope());
274         plugin.setPrototype(getPrototype(Plugin.class));
275         plugin.add(mimeTypeAppPdf);
276         plugin.add(mimeTypeTxtPdf);
277         plugins_.add(plugin);
278 
279         plugin = new Plugin("Microsoft Edge PDF Viewer", "Portable Document Format", "internal-pdf-viewer");
280         plugin.setParentScope(getParentScope());
281         plugin.setPrototype(getPrototype(Plugin.class));
282         plugin.add(mimeTypeAppPdf);
283         plugin.add(mimeTypeTxtPdf);
284         plugins_.add(plugin);
285 
286         plugin = new Plugin("WebKit built-in PDF", "Portable Document Format", "internal-pdf-viewer");
287         plugin.setParentScope(getParentScope());
288         plugin.setPrototype(getPrototype(Plugin.class));
289         plugin.add(mimeTypeAppPdf);
290         plugin.add(mimeTypeTxtPdf);
291         plugins_.add(plugin);
292     }
293 
294     /**
295      * Returns the {@code mimeTypes} property.
296      *
297      * @return the {@code mimeTypes} property
298      */
299     @JsxGetter
300     public MimeTypeArray getMimeTypes() {
301         initPluginsAndMimeTypes();
302         return mimeTypes_;
303     }
304 
305     /**
306      * Returns whether Java is enabled. Always returns {@code false}.
307      *
308      * @return {@code false}
309      */
310     @JsxFunction
311     public boolean javaEnabled() {
312         return false;
313     }
314 
315     /**
316      * Returns {@code false} as data tainting support is not enabled in HtmlUnit.
317      *
318      * @return {@code false}
319      */
320     @JsxFunction({FF, FF_ESR})
321     public boolean taintEnabled() {
322         return false;
323     }
324 
325     /**
326      * Returns the {@code geolocation} property.
327      *
328      * @return the {@code geolocation} property
329      */
330     @JsxGetter
331     public Geolocation getGeolocation() {
332         final Geolocation geolocation = new Geolocation();
333         geolocation.setParentScope(getParentScope());
334         geolocation.setPrototype(getPrototype(geolocation.getClass()));
335         return geolocation;
336     }
337 
338     /**
339      * Returns whether the browser supports inline display of PDF files when navigating to them.
340      *
341      * @return {@code true} if inline PDF viewing is supported
342      */
343     @JsxGetter
344     public boolean isPdfViewerEnabled() {
345         return true;
346     }
347 
348     /**
349      * Returns the {@code buildID} property.
350      *
351      * @return the {@code buildID} property
352      */
353     @JsxGetter({FF, FF_ESR})
354     public String getBuildID() {
355         return getBrowserVersion().getBuildId();
356     }
357 
358     /**
359      * Returns the {@code vendor} property.
360      *
361      * @return the {@code vendor} property
362      */
363     @JsxGetter
364     public String getVendor() {
365         return getBrowserVersion().getVendor();
366     }
367 
368     /**
369      * Returns the {@code vendorSub} property.
370      *
371      * @return the {@code vendorSub} property
372      */
373     @JsxGetter
374     public String getVendorSub() {
375         return "";
376     }
377 
378     /**
379      * Returns the {@code doNotTrack} property.
380      *
381      * @return the {@code doNotTrack} property
382      */
383     @JsxGetter
384     public Object getDoNotTrack() {
385         final WebClient client = getWindow().getWebWindow().getWebClient();
386         if (client.getOptions().isDoNotTrackEnabled()) {
387             return 1;
388         }
389         if (client.getBrowserVersion().hasFeature(JS_NAVIGATOR_DO_NOT_TRACK_UNSPECIFIED)) {
390             return "unspecified";
391         }
392         return null;
393     }
394 
395     /**
396      * Returns the {@code oscpu} property.
397      *
398      * @return the {@code oscpu} property
399      */
400     @JsxGetter({FF, FF_ESR})
401     public String getOscpu() {
402         return "Windows NT 6.1";
403     }
404 
405     /**
406      * Returns the {@code connection} property.
407      *
408      * @return the {@code connection} property
409      */
410     @JsxGetter({CHROME, EDGE})
411     public NetworkInformation getConnection() {
412         final NetworkInformation networkInformation = new NetworkInformation();
413         networkInformation.setPrototype(getPrototype(networkInformation.getClass()));
414         networkInformation.setParentScope(getParentScope());
415         return networkInformation;
416     }
417 
418     /**
419      * Returns the {@code mediaDevices} property.
420      *
421      * @return the {@code mediaDevices} property
422      */
423     @JsxGetter
424     public MediaDevices getMediaDevices() {
425         if (mediaDevices_ == null) {
426             mediaDevices_ = new MediaDevices();
427             mediaDevices_.setPrototype(getPrototype(mediaDevices_.getClass()));
428             mediaDevices_.setParentScope(getParentScope());
429         }
430         return mediaDevices_;
431     }
432 
433     /**
434      * The {@code sendBeacon()} method.
435      * <p>
436      * Note: real browsers queue the beacon and return immediately, continuing to
437      * attempt delivery even after the page unloads. This implementation instead sends
438      * the request synchronously (mirroring how {@code HyperlinkElementSupport} already
439      * handles hyperlink-auditing/{@code ping} requests and swallows any network error,
440      * since there is no caller left to report it to by the time
441      * a real browser would encounter it either.
442      * </p>
443      *
444      * @param url the URL to send the data to
445      * @param data the data to send; USVString, {@link Blob}, {@link URLSearchParams},
446      *             {@code FormData}, and ArrayBufferView are supported, per spec
447      * @return {@code true} if the browser successfully queued the data for transfer,
448      *         {@code false} otherwise (e.g. the URL could not be resolved)
449      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon">
450      *      MDN Documentation</a>
451      */
452     @JsxFunction
453     public boolean sendBeacon(final String url, final Object data) {
454         final Window window = getWindow();
455         final WebWindow webWindow = window.getWebWindow();
456         final HtmlPage page = (HtmlPage) webWindow.getEnclosedPage();
457         final URL pageUrl = page.getUrl();
458 
459         final URL targetUrl;
460         try {
461             targetUrl = page.getFullyQualifiedUrl(url);
462         }
463         catch (final MalformedURLException e) {
464             if (LOG.isInfoEnabled()) {
465                 LOG.info("sendBeacon(): invalid url '" + url + "'", e);
466             }
467             return false;
468         }
469 
470         final WebRequest request = new WebRequest(targetUrl, HttpMethod.POST);
471 
472         // a beacon is always a POST, so per Fetch's "Origin header" rules this is
473         // added unconditionally, same as HyperlinkElementSupport's ping request
474         try {
475             request.setRefererHeader(pageUrl);
476             request.setAdditionalHeader(HttpHeader.ORIGIN,
477                     UrlUtils.getUrlWithProtocolAndAuthority(pageUrl).toExternalForm());
478         }
479         catch (final MalformedURLException e) {
480             if (LOG.isInfoEnabled()) {
481                 LOG.info("sendBeacon(): invalid origin url '" + pageUrl + "'", e);
482             }
483         }
484 
485         // Sec-Fetch-* support (https://www.w3.org/TR/fetch-metadata/): a beacon has no
486         // destination and is always no-cors, matching hyperlink-auditing (ping) requests.
487         request.setFetchDestination(WebRequest.FetchDestination.EMPTY);
488         request.setFetchModeOverride(WebRequest.FetchMode.NO_CORS);
489         request.setRequestingUrl(pageUrl);
490 
491         fillRequestBody(request, data);
492 
493         final WebClient webClient = webWindow.getWebClient();
494         try {
495             webClient.loadWebResponse(request);
496         }
497         catch (final IOException e) {
498             if (LOG.isInfoEnabled()) {
499                 LOG.info("sendBeacon(): request failed", e);
500             }
501             // per spec this still returns true - queuing succeeded even if delivery didn't
502         }
503         return true;
504     }
505 
506     /**
507      * Fills in the request body for {@link #sendBeacon(String, Object)}, dispatching on
508      * the data's type the same way {@code XMLHttpRequest#prepareRequestContent} does for
509      * the body types the Beacon spec actually allows (unlike XHR, this excludes
510      * {@code Document} types).
511      *
512      * @param request the request to fill in
513      * @param data the data passed to {@code sendBeacon()}, possibly {@code null}/undefined
514      */
515     private static void fillRequestBody(final WebRequest request, final Object data) {
516         if (data == null || JavaScriptEngine.isUndefined(data)) {
517             return;
518         }
519 
520         if (data instanceof FormData formData) {
521             formData.fillRequest(request);
522         }
523         else if (data instanceof URLSearchParams params) {
524             params.fillRequest(request);
525             request.setCharset(UTF_8);
526             request.addHint(HttpHint.IncludeCharsetInContentTypeHeader);
527         }
528         else if (data instanceof Blob blob) {
529             blob.fillRequest(request);
530         }
531         else if (data instanceof NativeArrayBufferView view) {
532             request.setRequestBody(new String(view.getBuffer().getBuffer(), UTF_8));
533             request.setEncodingType(null);
534         }
535         else {
536             final String body = JavaScriptEngine.toString(data);
537             if (!body.isEmpty()) {
538                 request.setRequestBody(body);
539                 request.setEncodingType(FormEncodingType.TEXT_PLAIN);
540                 request.setCharset(UTF_8);
541                 request.addHint(HttpHint.IncludeCharsetInContentTypeHeader);
542             }
543         }
544     }
545 }