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