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