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.net.MalformedURLException;
18  import java.util.List;
19  import java.util.UUID;
20  
21  import org.apache.commons.lang3.StringUtils;
22  import org.htmlunit.Page;
23  import org.htmlunit.WebWindow;
24  import org.htmlunit.corejs.javascript.Scriptable;
25  import org.htmlunit.javascript.HtmlUnitScriptable;
26  import org.htmlunit.javascript.JavaScriptEngine;
27  import org.htmlunit.javascript.configuration.JsxClass;
28  import org.htmlunit.javascript.configuration.JsxConstructor;
29  import org.htmlunit.javascript.configuration.JsxConstructorAlias;
30  import org.htmlunit.javascript.configuration.JsxFunction;
31  import org.htmlunit.javascript.configuration.JsxGetter;
32  import org.htmlunit.javascript.configuration.JsxSetter;
33  import org.htmlunit.javascript.configuration.JsxStaticFunction;
34  import org.htmlunit.javascript.host.file.Blob;
35  import org.htmlunit.javascript.host.file.File;
36  import org.htmlunit.util.NameValuePair;
37  import org.htmlunit.util.UrlUtils;
38  
39  /**
40   * JavaScript host object for {@code URL}.
41   *
42   * @author Ahmed Ashour
43   * @author Ronald Brill
44   * @author cd alexndr
45   * @author Lai Quang Duong
46   *
47   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/URL">MDN Documentation</a>
48   */
49  @JsxClass
50  public class URL extends HtmlUnitScriptable {
51  
52      private java.net.URL url_;
53  
54      /**
55       * Creates an instance of this object.
56       *
57       * @param url a string representing an absolute or relative URL.
58       *        If {@code url} is a relative URL, {@code base} is required and will be used
59       *        as the base URL. If {@code url} is an absolute URL, a given {@code base} will be ignored.
60       * @param base a string representing the base URL to use when {@code url}
61       *        is a relative URL. If not specified, it defaults to {@code ''}.
62       */
63      @JsxConstructor
64      @JsxConstructorAlias(alias = "webkitURL")
65      public void jsConstructor(final String url, final Object base) {
66          String baseStr = null;
67          if (!JavaScriptEngine.isUndefined(base)) {
68              baseStr = JavaScriptEngine.toString(base);
69          }
70  
71          try {
72              if (org.htmlunit.util.StringUtils.isBlank(baseStr)) {
73                  url_ = UrlUtils.toUrlUnsafe(url);
74              }
75              else {
76                  final java.net.URL baseUrl = UrlUtils.toUrlUnsafe(baseStr);
77                  url_ = UrlUtils.toUrlUnsafe(UrlUtils.resolveUrl(baseUrl, url));
78              }
79              url_ = UrlUtils.removeRedundantPort(url_);
80          }
81          catch (final MalformedURLException e) {
82              throw JavaScriptEngine.typeError(e.toString());
83          }
84      }
85  
86      /**
87       * The URL.createObjectURL() static method creates a {@code DOMString} containing a URL
88       * representing the object given as parameter.
89       * The new object URL represents the specified {@link File} object or {@link Blob} object
90       * and is registered in the {@link org.htmlunit.BlobUrlStore user-agent-wide blob URL store},
91       * so it can be resolved from any document of the same client.
92       *
93       * @param fileOrBlob the {@link File} or {@link Blob} to create an object URL for
94       * @return the object URL, or {@code null} if the argument is not a supported type
95       * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL_static">MDN Documentation</a>
96       */
97      @JsxStaticFunction
98      public static String createObjectURL(final Object fileOrBlob) {
99          if (!(fileOrBlob instanceof Blob blob)) {
100             throw JavaScriptEngine.typeError("URL.createObjectURL: argument 1 is not a Blob.");
101         }
102 
103         final WebWindow webWindow = getWindow(blob).getWebWindow();
104         final Page page = webWindow.getEnclosedPage();
105         final java.net.URL pageUrl = page.getUrl();
106 
107         String origin = "null";
108         if (pageUrl != UrlUtils.URL_ABOUT_BLANK) {
109             final int port = pageUrl.getPort();
110             if (port < 0 || port == pageUrl.getDefaultPort()) {
111                 origin = pageUrl.getProtocol() + "://" + pageUrl.getHost();
112             }
113             else {
114                 origin = pageUrl.getProtocol() + "://" + pageUrl.getHost() + ':' + port;
115             }
116         }
117 
118         final String blobUrl = "blob:" + origin + "/" + UUID.randomUUID();
119         webWindow.getWebClient().getBlobUrlStore().put(blobUrl, blob, page);
120         return blobUrl;
121     }
122 
123     /**
124      * Revokes a {@code blob:} URL, removing its entry from the blob URL store.
125      *
126      * @param objectURL the object URL previously returned by {@link #createObjectURL(Object)}
127      *
128      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/URL/revokeObjectURL_static">MDN Documentation</a>
129      */
130     @JsxStaticFunction
131     public static void revokeObjectURL(final Scriptable objectURL) {
132         final String url = JavaScriptEngine.toString(objectURL);
133         if (!url.startsWith("blob:")) {
134             return;
135         }
136         getWindow(objectURL).getWebWindow().getWebClient().getBlobUrlStore().remove(url);
137     }
138 
139     /**
140      * Returns the hash portion of the URL, containing a {@code #} followed by the fragment identifier.
141      *
142      * @return the hash portion of the URL, or an empty string if there is no fragment
143      */
144     @JsxGetter
145     public String getHash() {
146         if (url_ == null) {
147             return null;
148         }
149         final String ref = url_.getRef();
150         return ref == null ? "" : "#" + ref;
151     }
152 
153     /**
154      * Sets the {@code hash} property.
155      *
156      * @param fragment the new hash value
157      * @throws MalformedURLException if the resulting URL is malformed
158      */
159     @JsxSetter
160     public void setHash(final String fragment) throws MalformedURLException {
161         if (url_ == null) {
162             return;
163         }
164         url_ = UrlUtils.getUrlWithNewRef(url_, org.htmlunit.util.StringUtils.isEmptyOrNull(fragment) ? null : fragment);
165     }
166 
167     /**
168      * Returns the host portion of the URL, consisting of the hostname and, if the port is non-empty,
169      * a {@code :} followed by the port.
170      *
171      * @return the host
172      */
173     @JsxGetter
174     public String getHost() {
175         if (url_ == null) {
176             return null;
177         }
178         final int port = url_.getPort();
179         return url_.getHost() + (port > 0 ? ":" + port : "");
180     }
181 
182     /**
183      * Sets the {@code host} property.
184      *
185      * @param host the new host value
186      * @throws MalformedURLException if the resulting URL is malformed
187      */
188     @JsxSetter
189     public void setHost(final String host) throws MalformedURLException {
190         if (url_ == null) {
191             return;
192         }
193 
194         String newHost = StringUtils.substringBefore(host, ':');
195         if (org.htmlunit.util.StringUtils.isEmptyOrNull(newHost)) {
196             return;
197         }
198 
199         try {
200             int ip = Integer.parseInt(newHost);
201             final StringBuilder ipString = new StringBuilder();
202             ipString.insert(0, ip % 256);
203             ipString.insert(0, '.');
204 
205             ip = ip / 256;
206             ipString.insert(0, ip % 256);
207             ipString.insert(0, '.');
208 
209             ip = ip / 256;
210             ipString.insert(0, ip % 256);
211             ipString.insert(0, '.');
212             ip = ip / 256;
213             ipString.insert(0, ip % 256);
214 
215             newHost = ipString.toString();
216         }
217         catch (final Exception expected) {
218             // back to string
219         }
220 
221         url_ = UrlUtils.getUrlWithNewHost(url_, newHost);
222 
223         final String newPort = StringUtils.substringAfter(host, ':');
224         if (org.htmlunit.util.StringUtils.isNotBlank(newHost)) {
225             try {
226                 url_ = UrlUtils.getUrlWithNewHostAndPort(url_, newHost, Integer.parseInt(newPort));
227             }
228             catch (final Exception expected) {
229                 // back to string
230             }
231         }
232         else {
233             url_ = UrlUtils.getUrlWithNewHost(url_, newHost);
234         }
235 
236         url_ = UrlUtils.removeRedundantPort(url_);
237     }
238 
239     /**
240      * Returns the hostname portion of the URL.
241      *
242      * @return the hostname
243      */
244     @JsxGetter
245     public String getHostname() {
246         if (url_ == null) {
247             return null;
248         }
249 
250         return UrlUtils.encodeAnchor(url_.getHost());
251     }
252 
253     /**
254      * Sets the {@code hostname} property.
255      *
256      * @param hostname the new hostname value
257      */
258     @JsxSetter
259     public void setHostname(String hostname) {
260         if (hostname != null) {
261             if (hostname.indexOf(' ') > -1) {
262                 return;
263             }
264 
265             final int idx = hostname.indexOf('#');
266             if (idx > -1) {
267                 hostname = hostname.substring(0, idx);
268             }
269         }
270 
271         if (org.htmlunit.util.StringUtils.isEmptyOrNull(hostname)) {
272             return;
273         }
274 
275         try {
276             url_ = UrlUtils.getUrlWithNewHost(url_, hostname);
277         }
278         catch (final MalformedURLException  e) {
279             // do nothing
280         }
281     }
282 
283     /**
284      * Returns the full URL as a string.
285      *
286      * @return the full URL
287      */
288     @JsxGetter
289     public String getHref() {
290         if (url_ == null) {
291             return null;
292         }
293 
294         return jsToString();
295     }
296 
297     /**
298      * Sets the {@code href} property, navigating to the new URL.
299      *
300      * @param href the new URL string
301      * @throws MalformedURLException if the URL is malformed
302      */
303     @JsxSetter
304     public void setHref(final String href) throws MalformedURLException {
305         if (url_ == null) {
306             return;
307         }
308 
309         url_ = UrlUtils.toUrlUnsafe(href);
310         url_ = UrlUtils.removeRedundantPort(url_);
311     }
312 
313     /**
314      * Returns the origin of the URL.
315      *
316      * @return the origin
317      */
318     @JsxGetter
319     public Object getOrigin() {
320         if (url_ == null) {
321             return null;
322         }
323 
324         if (url_.getPort() < 0 || url_.getPort() == url_.getDefaultPort()) {
325             return url_.getProtocol() + "://" + url_.getHost();
326         }
327 
328         return url_.getProtocol() + "://" + url_.getHost() + ':' + url_.getPort();
329     }
330 
331     /**
332      * Returns a {@link URLSearchParams} object providing access to the decoded query arguments of the URL.
333      *
334      * @return the search params
335      */
336     @JsxGetter
337     public URLSearchParams getSearchParams() {
338         if (url_ == null) {
339             return null;
340         }
341 
342         final URLSearchParams searchParams = new URLSearchParams(this);
343         searchParams.setParentScope(getParentScope());
344         searchParams.setPrototype(getPrototype(searchParams.getClass()));
345         return searchParams;
346     }
347 
348     /**
349      * Returns the password specified before the domain name.
350      *
351      * @return the password, or an empty string if none is specified
352      */
353     @JsxGetter
354     public String getPassword() {
355         if (url_ == null) {
356             return null;
357         }
358 
359         final String userInfo = url_.getUserInfo();
360         if (userInfo != null) {
361             final int idx = userInfo.indexOf(':');
362             if (idx > -1) {
363                 return userInfo.substring(idx + 1);
364             }
365         }
366 
367         return "";
368     }
369 
370     /**
371      * Sets the {@code password} property.
372      *
373      * @param password the new password value
374      * @throws MalformedURLException if the resulting URL is malformed
375      */
376     @JsxSetter
377     public void setPassword(final String password) throws MalformedURLException {
378         if (url_ == null) {
379             return;
380         }
381 
382         url_ = UrlUtils.getUrlWithNewUserPassword(url_, password.isEmpty() ? null : password);
383     }
384 
385     /**
386      * Returns the pathname portion of the URL.
387      *
388      * @return the pathname
389      */
390     @JsxGetter
391     public String getPathname() {
392         if (url_ == null) {
393             return null;
394         }
395 
396         final String path = url_.getPath();
397         return path.isEmpty() ? "/" : path;
398     }
399 
400     /**
401      * Sets the {@code pathname} property.
402      *
403      * @param path the new pathname value
404      * @throws MalformedURLException if the resulting URL is malformed
405      */
406     @JsxSetter
407     public void setPathname(final String path) throws MalformedURLException {
408         if (url_ == null) {
409             return;
410         }
411 
412         url_ = UrlUtils.getUrlWithNewPath(url_, path.startsWith("/") ? path : "/" + path);
413     }
414 
415     /**
416      * Returns the port number of the URL, or an empty string if no explicit port is specified.
417      *
418      * @return the port, or an empty string
419      */
420     @JsxGetter
421     public String getPort() {
422         if (url_ == null) {
423             return null;
424         }
425 
426         final int port = url_.getPort();
427         return port == -1 ? "" : Integer.toString(port);
428     }
429 
430     /**
431      * Sets the {@code port} property.
432      *
433      * @param port the new port value, or an empty string to remove the port
434      * @throws MalformedURLException if the resulting URL is malformed
435      */
436     @JsxSetter
437     public void setPort(final String port) throws MalformedURLException {
438         if (url_ == null) {
439             return;
440         }
441         final int portInt = port.isEmpty() ? -1 : Integer.parseInt(port);
442         url_ = UrlUtils.getUrlWithNewPort(url_, portInt);
443         url_ = UrlUtils.removeRedundantPort(url_);
444     }
445 
446     /**
447      * Returns the protocol scheme of the URL, including the trailing {@code :}.
448      *
449      * @return the protocol
450      */
451     @JsxGetter
452     public String getProtocol() {
453         if (url_ == null) {
454             return null;
455         }
456         final String protocol = url_.getProtocol();
457         return protocol.isEmpty() ? "" : (protocol + ":");
458     }
459 
460     /**
461      * Sets the {@code protocol} property.
462      *
463      * @param protocol the new protocol value
464      * @throws MalformedURLException if the resulting URL is malformed
465      */
466     @JsxSetter
467     public void setProtocol(final String protocol) throws MalformedURLException {
468         if (url_ == null || protocol.isEmpty()) {
469             return;
470         }
471 
472         final String bareProtocol = org.htmlunit.util.StringUtils.substringBefore(protocol, ":").trim();
473         if (!UrlUtils.isValidScheme(bareProtocol)) {
474             return;
475         }
476         if (!UrlUtils.isSpecialScheme(bareProtocol)) {
477             return;
478         }
479 
480         try {
481             url_ = UrlUtils.getUrlWithNewProtocol(url_, bareProtocol);
482             url_ = UrlUtils.removeRedundantPort(url_);
483         }
484         catch (final MalformedURLException ignored) {
485             // ignore
486         }
487     }
488 
489     /**
490      * Returns the query string, containing a {@code ?} followed by the URL's parameters.
491      *
492      * @return the search string, or an empty string if none
493      */
494     @JsxGetter
495     public String getSearch() {
496         if (url_ == null) {
497             return null;
498         }
499         final String search = url_.getQuery();
500         return search == null ? "" : "?" + search;
501     }
502 
503     /**
504      * Sets the {@code search} property.
505      *
506      * @param search the new search string
507      * @throws MalformedURLException if the resulting URL is malformed
508      */
509     @JsxSetter
510     public void setSearch(final String search) throws MalformedURLException {
511         if (url_ == null) {
512             return;
513         }
514 
515         String query;
516         if (search == null
517                 || org.htmlunit.util.StringUtils.equalsChar('?', search)
518                 || org.htmlunit.util.StringUtils.isEmptyString(search)) {
519             query = null;
520         }
521         else {
522             if (search.charAt(0) == '?') {
523                 query = search.substring(1);
524             }
525             else {
526                 query = search;
527             }
528             query = UrlUtils.encodeQuery(query);
529         }
530 
531         url_ = UrlUtils.getUrlWithNewQuery(url_, query);
532     }
533 
534     /**
535      * Sets the {@code search} property from a list of {@link NameValuePair}s.
536      *
537      * @param nameValuePairs the pairs to encode as the query string
538      * @throws MalformedURLException if the resulting URL is malformed
539      */
540     public void setSearch(final List<NameValuePair> nameValuePairs) throws MalformedURLException {
541         final StringBuilder newSearch = new StringBuilder();
542         for (final NameValuePair nameValuePair : nameValuePairs) {
543             if (newSearch.length() > 0) {
544                 newSearch.append('&');
545             }
546             newSearch
547                 .append(UrlUtils.encodeQueryPart(nameValuePair.getName()))
548                 .append('=')
549                 .append(UrlUtils.encodeQueryPart(nameValuePair.getValue()));
550         }
551 
552         url_ = UrlUtils.getUrlWithNewQuery(url_, newSearch.toString());
553     }
554 
555     /**
556      * Returns the username specified before the domain name.
557      *
558      * @return the username, or an empty string if none is specified
559      */
560     @JsxGetter
561     public String getUsername() {
562         if (url_ == null) {
563             return null;
564         }
565 
566         final String userInfo = url_.getUserInfo();
567         if (userInfo == null) {
568             return "";
569         }
570 
571         return StringUtils.substringBefore(userInfo, ':');
572     }
573 
574     /**
575      * Sets the {@code username} property.
576      *
577      * @param username the new username value
578      * @throws MalformedURLException if the resulting URL is malformed
579      */
580     @JsxSetter
581     public void setUsername(final String username) throws MalformedURLException {
582         if (url_ == null) {
583             return;
584         }
585         url_ = UrlUtils.getUrlWithNewUserName(url_, username.isEmpty() ? null : username);
586     }
587 
588     /**
589      * Returns the default string representation of this URL.
590      *
591      * @param hint the type hint
592      * @return the URL as a string
593      * @see org.htmlunit.javascript.HtmlUnitScriptable#getDefaultValue(java.lang.Class)
594      */
595     @Override
596     public Object getDefaultValue(final Class<?> hint) {
597         if (url_ == null) {
598             return super.getDefaultValue(hint);
599         }
600 
601         if (org.htmlunit.util.StringUtils.isEmptyOrNull(url_.getPath())) {
602             return url_.toExternalForm() + "/";
603         }
604         return url_.toExternalForm();
605     }
606 
607     /**
608      * Returns a serialized version of the URL. In practice this is equivalent to {@link #jsToString()}.
609      *
610      * @return the serialized URL string
611      */
612     @JsxFunction
613     public String toJSON() {
614         return jsToString();
615     }
616 
617     /**
618      * Returns the URL as a string.
619      *
620      * @return the URL string
621      */
622     @JsxFunction(functionName = "toString")
623     public String jsToString() {
624         if (org.htmlunit.util.StringUtils.isEmptyOrNull(url_.getPath())) {
625             try {
626                 return UrlUtils.getUrlWithNewPath(url_, "/").toExternalForm();
627             }
628             catch (final MalformedURLException e) {
629                 return url_.toExternalForm();
630             }
631         }
632         return url_.toExternalForm();
633     }
634 }