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  
22  import org.apache.commons.lang3.StringUtils;
23  import org.htmlunit.corejs.javascript.Context;
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   * A JavaScript object for {@code URL}.
41   *
42   * @author Ahmed Ashour
43   * @author Ronald Brill
44   * @author cd alexndr
45   * @author Lai Quang Duong
46   */
47  @JsxClass
48  public class URL extends HtmlUnitScriptable {
49  
50      private java.net.URL url_;
51  
52      /**
53       * Creates an instance.
54       * @param url a string representing an absolute or relative URL.
55       *        If url is a relative URL, base is required, and will be used
56       *        as the base URL. If url is an absolute URL, a given base will be ignored.
57       * @param base a string representing the base URL to use in case url
58       *        is a relative URL. If not specified, it defaults to ''.
59       */
60      @JsxConstructor
61      @JsxConstructorAlias(alias = "webkitURL")
62      public void jsConstructor(final String url, final Object base) {
63          String baseStr = null;
64          if (!JavaScriptEngine.isUndefined(base)) {
65              baseStr = JavaScriptEngine.toString(base);
66          }
67  
68          try {
69              if (org.htmlunit.util.StringUtils.isBlank(baseStr)) {
70                  url_ = UrlUtils.toUrlUnsafe(url);
71              }
72              else {
73                  final java.net.URL baseUrl = UrlUtils.toUrlUnsafe(baseStr);
74                  url_ = UrlUtils.toUrlUnsafe(UrlUtils.resolveUrl(baseUrl, url));
75              }
76              url_ = UrlUtils.removeRedundantPort(url_);
77          }
78          catch (final MalformedURLException e) {
79              throw JavaScriptEngine.typeError(e.toString());
80          }
81      }
82  
83      /**
84       * The URL.createObjectURL() static method creates a DOMString containing a URL
85       * representing the object given in parameter.
86       * The URL lifetime is tied to the document in the window on which it was created.
87       * The new object URL represents the specified File object or Blob object.
88       *
89       * @param fileOrBlob Is a File object or a Blob object to create a object URL for.
90       * @return the url
91       */
92      @JsxStaticFunction
93      public static String createObjectURL(final Object fileOrBlob) {
94          if (fileOrBlob instanceof File file) {
95              return getWindow(file).getDocument().generateBlobUrl(file);
96          }
97  
98          if (fileOrBlob instanceof Blob blob) {
99              return getWindow(blob).getDocument().generateBlobUrl(blob);
100         }
101 
102         return null;
103     }
104 
105     /**
106      * @param objectURL String representing the object URL that was
107      *          created by calling URL.createObjectURL().
108      */
109     @JsxStaticFunction
110     public static void revokeObjectURL(final Scriptable objectURL) {
111         getWindow(objectURL).getDocument().revokeBlobUrl(Context.toString(objectURL));
112     }
113 
114     /**
115      * @return hash property of the URL containing a '#' followed by the fragment identifier of the URL.
116      */
117     @JsxGetter
118     public String getHash() {
119         if (url_ == null) {
120             return null;
121         }
122         final String ref = url_.getRef();
123         return ref == null ? "" : "#" + ref;
124     }
125 
126     /**
127      * Sets the {@code hash} property.
128      * @param fragment the {@code hash} property
129      */
130     @JsxSetter
131     public void setHash(final String fragment) throws MalformedURLException {
132         if (url_ == null) {
133             return;
134         }
135         url_ = UrlUtils.getUrlWithNewRef(url_, org.htmlunit.util.StringUtils.isEmptyOrNull(fragment) ? null : fragment);
136     }
137 
138     /**
139      * @return the host, that is the hostname, and then, if the port of the URL is nonempty,
140      *         a ':', followed by the port of the URL.
141      */
142     @JsxGetter
143     public String getHost() {
144         if (url_ == null) {
145             return null;
146         }
147         final int port = url_.getPort();
148         return url_.getHost() + (port > 0 ? ":" + port : "");
149     }
150 
151     /**
152      * Sets the {@code host} property.
153      * @param host the {@code host} property
154      */
155     @JsxSetter
156     public void setHost(final String host) throws MalformedURLException {
157         if (url_ == null) {
158             return;
159         }
160 
161         String newHost = StringUtils.substringBefore(host, ':');
162         if (org.htmlunit.util.StringUtils.isEmptyOrNull(newHost)) {
163             return;
164         }
165 
166         try {
167             int ip = Integer.parseInt(newHost);
168             final StringBuilder ipString = new StringBuilder();
169             ipString.insert(0, ip % 256);
170             ipString.insert(0, '.');
171 
172             ip = ip / 256;
173             ipString.insert(0, ip % 256);
174             ipString.insert(0, '.');
175 
176             ip = ip / 256;
177             ipString.insert(0, ip % 256);
178             ipString.insert(0, '.');
179             ip = ip / 256;
180             ipString.insert(0, ip % 256);
181 
182             newHost = ipString.toString();
183         }
184         catch (final Exception expected) {
185             // back to string
186         }
187 
188         url_ = UrlUtils.getUrlWithNewHost(url_, newHost);
189 
190         final String newPort = StringUtils.substringAfter(host, ':');
191         if (org.htmlunit.util.StringUtils.isNotBlank(newHost)) {
192             try {
193                 url_ = UrlUtils.getUrlWithNewHostAndPort(url_, newHost, Integer.parseInt(newPort));
194             }
195             catch (final Exception expected) {
196                 // back to string
197             }
198         }
199         else {
200             url_ = UrlUtils.getUrlWithNewHost(url_, newHost);
201         }
202 
203         url_ = UrlUtils.removeRedundantPort(url_);
204     }
205 
206     /**
207      * @return the host, that is the hostname, and then, if the port of the URL is nonempty,
208      *         a ':', followed by the port of the URL.
209      */
210     @JsxGetter
211     public String getHostname() {
212         if (url_ == null) {
213             return null;
214         }
215 
216         return UrlUtils.encodeAnchor(url_.getHost());
217     }
218 
219     /**
220      * Sets the {@code hostname} property.
221      * @param hostname the {@code hostname} property
222      */
223     @JsxSetter
224     public void setHostname(final String hostname) throws MalformedURLException {
225         if (getBrowserVersion().hasFeature(JS_ANCHOR_HOSTNAME_IGNORE_BLANK)) {
226             if (!org.htmlunit.util.StringUtils.isBlank(hostname)) {
227                 url_ = UrlUtils.getUrlWithNewHost(url_, hostname);
228             }
229         }
230         else if (!org.htmlunit.util.StringUtils.isEmptyOrNull(hostname)) {
231             url_ = UrlUtils.getUrlWithNewHost(url_, hostname);
232         }
233     }
234 
235     /**
236      * @return whole URL
237      */
238     @JsxGetter
239     public String getHref() {
240         if (url_ == null) {
241             return null;
242         }
243 
244         return jsToString();
245     }
246 
247     /**
248      * Sets the {@code href} property.
249      * @param href the {@code href} property
250      */
251     @JsxSetter
252     public void setHref(final String href) throws MalformedURLException {
253         if (url_ == null) {
254             return;
255         }
256 
257         url_ = UrlUtils.toUrlUnsafe(href);
258         url_ = UrlUtils.removeRedundantPort(url_);
259     }
260 
261     /**
262      * @return the origin
263      */
264     @JsxGetter
265     public Object getOrigin() {
266         if (url_ == null) {
267             return null;
268         }
269 
270         if (url_.getPort() < 0 || url_.getPort() == url_.getDefaultPort()) {
271             return url_.getProtocol() + "://" + url_.getHost();
272         }
273 
274         return url_.getProtocol() + "://" + url_.getHost() + ':' + url_.getPort();
275     }
276 
277     /**
278      * @return a URLSearchParams object allowing access to the GET decoded query arguments contained in the URL.
279      */
280     @JsxGetter
281     public URLSearchParams getSearchParams() {
282         if (url_ == null) {
283             return null;
284         }
285 
286         final URLSearchParams searchParams = new URLSearchParams(this);
287         searchParams.setParentScope(getParentScope());
288         searchParams.setPrototype(getPrototype(searchParams.getClass()));
289         return searchParams;
290     }
291 
292     /**
293      * @return the password specified before the domain name.
294      */
295     @JsxGetter
296     public String getPassword() {
297         if (url_ == null) {
298             return null;
299         }
300 
301         final String userInfo = url_.getUserInfo();
302         final int idx = userInfo == null ? -1 : userInfo.indexOf(':');
303         return idx == -1 ? "" : userInfo.substring(idx + 1);
304     }
305 
306     /**
307      * Sets the {@code password} property.
308      * @param password the {@code password} property
309      */
310     @JsxSetter
311     public void setPassword(final String password) throws MalformedURLException {
312         if (url_ == null) {
313             return;
314         }
315 
316         url_ = UrlUtils.getUrlWithNewUserPassword(url_, password.isEmpty() ? null : password);
317     }
318 
319     /**
320      * @return a URLSearchParams object allowing access to the GET decoded query arguments contained in the URL.
321      */
322     @JsxGetter
323     public String getPathname() {
324         if (url_ == null) {
325             return null;
326         }
327 
328         final String path = url_.getPath();
329         return path.isEmpty() ? "/" : path;
330     }
331 
332     /**
333      * Sets the {@code path} property.
334      * @param path the {@code path} property
335      */
336     @JsxSetter
337     public void setPathname(final String path) throws MalformedURLException {
338         if (url_ == null) {
339             return;
340         }
341 
342         url_ = UrlUtils.getUrlWithNewPath(url_, path.startsWith("/") ? path : "/" + path);
343     }
344 
345     /**
346      * @return the port number of the URL. If the URL does not contain an explicit port number,
347      *         it will be set to ''
348      */
349     @JsxGetter
350     public String getPort() {
351         if (url_ == null) {
352             return null;
353         }
354 
355         final int port = url_.getPort();
356         return port == -1 ? "" : Integer.toString(port);
357     }
358 
359     /**
360      * Sets the {@code port} property.
361      * @param port the {@code port} property
362      */
363     @JsxSetter
364     public void setPort(final String port) throws MalformedURLException {
365         if (url_ == null) {
366             return;
367         }
368         final int portInt = port.isEmpty() ? -1 : Integer.parseInt(port);
369         url_ = UrlUtils.getUrlWithNewPort(url_, portInt);
370         url_ = UrlUtils.removeRedundantPort(url_);
371     }
372 
373     /**
374      * @return the protocol scheme of the URL, including the final ':'.
375      */
376     @JsxGetter
377     public String getProtocol() {
378         if (url_ == null) {
379             return null;
380         }
381         final String protocol = url_.getProtocol();
382         return protocol.isEmpty() ? "" : (protocol + ":");
383     }
384 
385     /**
386      * Sets the {@code protocol} property.
387      * @param protocol the {@code protocol} property
388      */
389     @JsxSetter
390     public void setProtocol(final String protocol) throws MalformedURLException {
391         if (url_ == null || protocol.isEmpty()) {
392             return;
393         }
394 
395         final String bareProtocol = org.htmlunit.util.StringUtils.substringBefore(protocol, ":").trim();
396         if (!UrlUtils.isValidScheme(bareProtocol)) {
397             return;
398         }
399         if (!UrlUtils.isSpecialScheme(bareProtocol)) {
400             return;
401         }
402 
403         try {
404             url_ = UrlUtils.getUrlWithNewProtocol(url_, bareProtocol);
405             url_ = UrlUtils.removeRedundantPort(url_);
406         }
407         catch (final MalformedURLException ignored) {
408             // ignore
409         }
410     }
411 
412     /**
413      * @return the query string containing a '?' followed by the parameters of the URL
414      */
415     @JsxGetter
416     public String getSearch() {
417         if (url_ == null) {
418             return null;
419         }
420         final String search = url_.getQuery();
421         return search == null ? "" : "?" + search;
422     }
423 
424     /**
425      * Sets the {@code search} property.
426      * @param search the {@code search} property
427      */
428     @JsxSetter
429     public void setSearch(final String search) throws MalformedURLException {
430         if (url_ == null) {
431             return;
432         }
433 
434         String query;
435         if (search == null
436                 || org.htmlunit.util.StringUtils.equalsChar('?', search)
437                 || org.htmlunit.util.StringUtils.isEmptyString(search)) {
438             query = null;
439         }
440         else {
441             if (search.charAt(0) == '?') {
442                 query = search.substring(1);
443             }
444             else {
445                 query = search;
446             }
447             query = UrlUtils.encodeQuery(query);
448         }
449 
450         url_ = UrlUtils.getUrlWithNewQuery(url_, query);
451     }
452 
453     /**
454      * Sets the {@code search} property based on {@link NameValuePair}'s.
455      * @param nameValuePairs the pairs
456      * @throws MalformedURLException in case of error
457      */
458     public void setSearch(final List<NameValuePair> nameValuePairs) throws MalformedURLException {
459         final StringBuilder newSearch = new StringBuilder();
460         for (final NameValuePair nameValuePair : nameValuePairs) {
461             if (newSearch.length() > 0) {
462                 newSearch.append('&');
463             }
464             newSearch
465                 .append(UrlUtils.encodeQueryPart(nameValuePair.getName()))
466                 .append('=')
467                 .append(UrlUtils.encodeQueryPart(nameValuePair.getValue()));
468         }
469 
470         url_ = UrlUtils.getUrlWithNewQuery(url_, newSearch.toString());
471     }
472 
473     /**
474      * @return the username specified before the domain name.
475      */
476     @JsxGetter
477     public String getUsername() {
478         if (url_ == null) {
479             return null;
480         }
481 
482         final String userInfo = url_.getUserInfo();
483         if (userInfo == null) {
484             return "";
485         }
486 
487         return StringUtils.substringBefore(userInfo, ':');
488     }
489 
490     /**
491      * Sets the {@code username} property.
492      * @param username the {@code username} property
493      */
494     @JsxSetter
495     public void setUsername(final String username) throws MalformedURLException {
496         if (url_ == null) {
497             return;
498         }
499         url_ = UrlUtils.getUrlWithNewUserName(url_, username.isEmpty() ? null : username);
500     }
501 
502     /**
503      * Calls for instance for implicit conversion to string.
504      * @see org.htmlunit.javascript.HtmlUnitScriptable#getDefaultValue(java.lang.Class)
505      * @param hint the type hint
506      * @return the default value
507      */
508     @Override
509     public Object getDefaultValue(final Class<?> hint) {
510         if (url_ == null) {
511             return super.getDefaultValue(hint);
512         }
513 
514         if (org.htmlunit.util.StringUtils.isEmptyOrNull(url_.getPath())) {
515             return url_.toExternalForm() + "/";
516         }
517         return url_.toExternalForm();
518     }
519 
520     /**
521      * @return a serialized version of the URL,
522      *         although in practice it seems to have the same effect as URL.toString().
523      */
524     @JsxFunction
525     public String toJSON() {
526         return jsToString();
527     }
528 
529     /**
530      * Returns the text of the URL.
531      * @return the text
532      */
533     @JsxFunction(functionName = "toString")
534     public String jsToString() {
535         if (org.htmlunit.util.StringUtils.isEmptyOrNull(url_.getPath())) {
536             try {
537                 return UrlUtils.getUrlWithNewPath(url_, "/").toExternalForm();
538             }
539             catch (final MalformedURLException e) {
540                 return url_.toExternalForm();
541             }
542         }
543         return url_.toExternalForm();
544     }
545 }