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_LOCATION_IGNORE_QUERY_FOR_ABOUT_PROTOCOL;
18  import static org.htmlunit.BrowserVersionFeatures.JS_LOCATION_RELOAD_REFERRER;
19  
20  import java.io.IOException;
21  import java.lang.reflect.Method;
22  import java.net.MalformedURLException;
23  import java.net.URL;
24  
25  import org.apache.commons.lang3.StringUtils;
26  import org.apache.commons.logging.Log;
27  import org.apache.commons.logging.LogFactory;
28  import org.htmlunit.BrowserVersion;
29  import org.htmlunit.Page;
30  import org.htmlunit.WebRequest;
31  import org.htmlunit.WebWindow;
32  import org.htmlunit.corejs.javascript.FunctionObject;
33  import org.htmlunit.corejs.javascript.ScriptableObject;
34  import org.htmlunit.html.HtmlPage;
35  import org.htmlunit.javascript.HtmlUnitScriptable;
36  import org.htmlunit.javascript.configuration.JsxClass;
37  import org.htmlunit.javascript.configuration.JsxConstructor;
38  import org.htmlunit.javascript.host.event.Event;
39  import org.htmlunit.javascript.host.event.HashChangeEvent;
40  import org.htmlunit.protocol.javascript.JavaScriptURLConnection;
41  import org.htmlunit.util.UrlUtils;
42  
43  /**
44   * A JavaScript object for {@code Location}.
45   *
46   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
47   * @author Michael Ottati
48   * @author Marc Guillemot
49   * @author Chris Erskine
50   * @author Daniel Gredler
51   * @author David K. Taylor
52   * @author Ahmed Ashour
53   * @author Ronald Brill
54   * @author Frank Danek
55   * @author Adam Afeltowicz
56   * @author Atsushi Nakagawa
57   * @author Lai Quang Duong
58   * @author Kanoko Yamamoto
59   *
60   * @see <a href="http://msdn.microsoft.com/en-us/library/ms535866.aspx">MSDN Documentation</a>
61   */
62  @JsxClass
63  public class Location extends HtmlUnitScriptable {
64  
65      private static final Log LOG = LogFactory.getLog(Location.class);
66      private static final String UNKNOWN = "null";
67  
68      /**
69       * The window which owns this location object.
70       */
71      private Window window_;
72  
73      private static final Method METHOD_ASSIGN;
74      private static final Method METHOD_RELOAD;
75      private static final Method METHOD_REPLACE;
76      private static final Method METHOD_TO_STRING;
77  
78      private static final Method GETTER_HASH;
79      private static final Method SETTER_HASH;
80  
81      private static final Method GETTER_HOST;
82      private static final Method SETTER_HOST;
83  
84      private static final Method GETTER_HOSTNAME;
85      private static final Method SETTER_HOSTNAME;
86  
87      private static final Method GETTER_HREF;
88      private static final Method SETTER_HREF;
89  
90      private static final Method GETTER_ORIGIN;
91  
92      private static final Method GETTER_PATHNAME;
93      private static final Method SETTER_PATHNAME;
94  
95      private static final Method GETTER_PORT;
96      private static final Method SETTER_PORT;
97  
98      private static final Method GETTER_PROTOCOL;
99      private static final Method SETTER_PROTOCOL;
100 
101     private static final Method GETTER_SEARCH;
102     private static final Method SETTER_SEARCH;
103 
104     static {
105         try {
106             METHOD_ASSIGN = Location.class.getDeclaredMethod("assign", String.class);
107             METHOD_RELOAD = Location.class.getDeclaredMethod("reload", boolean.class);
108             METHOD_REPLACE = Location.class.getDeclaredMethod("replace", String.class);
109             METHOD_TO_STRING = Location.class.getDeclaredMethod("jsToString");
110 
111             GETTER_HASH = Location.class.getDeclaredMethod("getHash");
112             SETTER_HASH = Location.class.getDeclaredMethod("setHash", String.class);
113 
114             GETTER_HOST = Location.class.getDeclaredMethod("getHost");
115             SETTER_HOST = Location.class.getDeclaredMethod("setHost", String.class);
116 
117             GETTER_HOSTNAME = Location.class.getDeclaredMethod("getHostname");
118             SETTER_HOSTNAME = Location.class.getDeclaredMethod("setHostname", String.class);
119 
120             GETTER_HREF = Location.class.getDeclaredMethod("getHref");
121             SETTER_HREF = Location.class.getDeclaredMethod("setHref", String.class);
122 
123             GETTER_ORIGIN = Location.class.getDeclaredMethod("getOrigin");
124 
125             GETTER_PATHNAME = Location.class.getDeclaredMethod("getPathname");
126             SETTER_PATHNAME = Location.class.getDeclaredMethod("setPathname", String.class);
127 
128             GETTER_PORT = Location.class.getDeclaredMethod("getPort");
129             SETTER_PORT = Location.class.getDeclaredMethod("setPort", String.class);
130 
131             GETTER_PROTOCOL = Location.class.getDeclaredMethod("getProtocol");
132             SETTER_PROTOCOL = Location.class.getDeclaredMethod("setProtocol", String.class);
133 
134             GETTER_SEARCH = Location.class.getDeclaredMethod("getSearch");
135             SETTER_SEARCH = Location.class.getDeclaredMethod("setSearch", String.class);
136         }
137         catch (NoSuchMethodException | SecurityException e) {
138             throw new RuntimeException(e);
139         }
140     }
141 
142     /**
143      * The current hash; we cache it locally because we don't want to modify the page's URL and
144      * force a page reload each time this changes.
145      */
146     private String hash_;
147 
148     /**
149      * Creates an instance.
150      */
151     @JsxConstructor
152     public void jsConstructor() {
153         final int attributes = ScriptableObject.PERMANENT | ScriptableObject.READONLY;
154 
155         FunctionObject functionObject = new FunctionObject(METHOD_ASSIGN.getName(), METHOD_ASSIGN, this);
156         defineProperty(METHOD_ASSIGN.getName(), functionObject, attributes);
157 
158         functionObject = new FunctionObject(METHOD_RELOAD.getName(), METHOD_RELOAD, this);
159         defineProperty(METHOD_RELOAD.getName(), functionObject, attributes);
160 
161         functionObject = new FunctionObject(METHOD_REPLACE.getName(), METHOD_REPLACE, this);
162         defineProperty(METHOD_REPLACE.getName(), functionObject, attributes);
163 
164         functionObject = new FunctionObject(METHOD_TO_STRING.getName(), METHOD_TO_STRING, this);
165         defineProperty("toString", functionObject, attributes);
166 
167         defineProperty("hash", null, GETTER_HASH, SETTER_HASH, attributes);
168         defineProperty("host", null, GETTER_HOST, SETTER_HOST, attributes);
169         defineProperty("hostname", null, GETTER_HOSTNAME, SETTER_HOSTNAME, attributes);
170         defineProperty("href", null, GETTER_HREF, SETTER_HREF, attributes);
171         defineProperty("origin", null, GETTER_ORIGIN, null, attributes);
172         defineProperty("pathname", null, GETTER_PATHNAME, SETTER_PATHNAME, attributes);
173         defineProperty("port", null, GETTER_PORT, SETTER_PORT, attributes);
174         defineProperty("protocol", null, GETTER_PROTOCOL, SETTER_PROTOCOL, attributes);
175         defineProperty("search", null, GETTER_SEARCH, SETTER_SEARCH, attributes);
176     }
177 
178     /**
179      * Initializes this Location.
180      *
181      * @param window the window that this location belongs to
182      * @param page the page that will become the enclosing page
183      */
184     public void initialize(final Window window, final Page page) {
185         window_ = window;
186         if (window_ != null && page != null) {
187             setHash(null, page.getUrl().getRef());
188         }
189     }
190 
191     /**
192      * {@inheritDoc}
193      */
194     @Override
195     public Object getDefaultValue(final Class<?> hint) {
196         if (getPrototype() != null
197                 && window_ != null
198                 && (hint == null || String.class.equals(hint))) {
199             return getHref();
200         }
201         return super.getDefaultValue(hint);
202     }
203 
204     /**
205      * Loads the new HTML document corresponding to the specified URL.
206      * @param url the location of the new HTML document to load
207      * @throws IOException if loading the specified location fails
208      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536342.aspx">MSDN Documentation</a>
209      */
210     public void assign(final String url) throws IOException {
211         setHref(url);
212     }
213 
214     /**
215      * Reloads the current page, possibly forcing retrieval from the server even if
216      * the browser cache contains the latest version of the document.
217      * @param force if {@code true}, force reload from server; otherwise, may reload from cache
218      * @throws IOException if there is a problem reloading the page
219      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536342.aspx">MSDN Documentation</a>
220      */
221     public void reload(final boolean force) throws IOException {
222         final WebWindow webWindow = window_.getWebWindow();
223         final HtmlPage htmlPage = (HtmlPage) webWindow.getEnclosedPage();
224         final WebRequest request = htmlPage.getWebResponse().getWebRequest();
225 
226         // update request url with location.href in case hash was changed
227         request.setUrl(new URL(getHref()));
228         if (webWindow.getWebClient().getBrowserVersion().hasFeature(JS_LOCATION_RELOAD_REFERRER)) {
229             request.setRefererHeader(htmlPage.getUrl());
230         }
231 
232         webWindow.getWebClient().download(webWindow, "", request, false, false, null, "JS location.reload");
233     }
234 
235     /**
236      * Reloads the window using the specified URL via a postponed action.
237      * @param url the new URL to use to reload the window
238      * @throws IOException if loading the specified location fails
239      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536712.aspx">MSDN Documentation</a>
240      */
241     public void replace(final String url) throws IOException {
242         window_.getWebWindow().getHistory().removeCurrent();
243         setHref(url);
244     }
245 
246     /**
247      * Returns the location URL.
248      * @return the location URL
249      */
250     public String jsToString() {
251         if (window_ != null) {
252             return getHref();
253         }
254         return "";
255     }
256 
257     /**
258      * Returns the location URL.
259      * @return the location URL
260      * @see <a href="http://msdn.microsoft.com/en-us/library/ms533867.aspx">MSDN Documentation</a>
261      */
262     public String getHref() {
263         final WebWindow webWindow = window_.getWebWindow();
264         final Page page = webWindow.getEnclosedPage();
265         if (page == null) {
266             return UNKNOWN;
267         }
268         try {
269             URL url = page.getUrl();
270             final String hash = getHash(true);
271             if (hash != null) {
272                 url = UrlUtils.getUrlWithNewRef(url, hash);
273             }
274             String s = url.toExternalForm();
275             if (s.startsWith("file:/") && !s.startsWith("file:///")) {
276                 // Java (sometimes?) returns file URLs with a single slash; however, browsers return
277                 // three slashes. See http://www.cyanwerks.com/file-url-formats.html for more info.
278                 s = "file:///" + s.substring("file:/".length());
279             }
280             return s;
281         }
282         catch (final MalformedURLException e) {
283             if (LOG.isErrorEnabled()) {
284                 LOG.error(e.getMessage(), e);
285             }
286             return page.getUrl().toExternalForm();
287         }
288     }
289 
290     /**
291      * Sets the location URL to an entirely new value.
292      * @param newLocation the new location URL
293      * @throws IOException if loading the specified location fails
294      * @see <a href="http://msdn.microsoft.com/en-us/library/ms533867.aspx">MSDN Documentation</a>
295      */
296     public void setHref(final String newLocation) throws IOException {
297         WebWindow webWindow = getWindowFromTopCallScope().getWebWindow();
298         final HtmlPage page = (HtmlPage) webWindow.getEnclosedPage();
299         if (newLocation.startsWith(JavaScriptURLConnection.JAVASCRIPT_PREFIX)) {
300             final String script = newLocation.substring(11);
301             page.executeJavaScript(script, "new location value", 1);
302             return;
303         }
304         try {
305             final BrowserVersion browserVersion = webWindow.getWebClient().getBrowserVersion();
306 
307             URL url = page.getFullyQualifiedUrl(newLocation);
308             // fix for empty url
309             if (StringUtils.isEmpty(newLocation)) {
310                 url = UrlUtils.getUrlWithNewRef(url, null);
311             }
312 
313             final WebRequest request = new WebRequest(url,
314                         browserVersion.getHtmlAcceptHeader(), browserVersion.getAcceptEncodingHeader());
315             request.setRefererHeader(page.getUrl());
316 
317             webWindow = window_.getWebWindow();
318             webWindow.getWebClient().download(webWindow, "", request, true, false, null, "JS set location");
319         }
320         catch (final MalformedURLException e) {
321             if (LOG.isErrorEnabled()) {
322                 LOG.error("setHref('" + newLocation + "') got MalformedURLException", e);
323             }
324             throw e;
325         }
326     }
327 
328     /**
329      * Returns the search portion of the location URL (the portion following the '?').
330      * @return the search portion of the location URL
331      * @see <a href="http://msdn.microsoft.com/en-us/library/ms534620.aspx">MSDN Documentation</a>
332      */
333     public String getSearch() {
334         final URL url = getUrl();
335         final String search = url.getQuery();
336         if (StringUtils.isEmpty(search)) {
337             return "";
338         }
339 
340         if (StringUtils.startsWithIgnoreCase(url.getProtocol(), UrlUtils.ABOUT)
341                 && window_.getWebWindow().getWebClient().getBrowserVersion()
342                                 .hasFeature(JS_LOCATION_IGNORE_QUERY_FOR_ABOUT_PROTOCOL)) {
343             return "";
344         }
345         return "?" + search;
346     }
347 
348     /**
349      * Sets the search portion of the location URL (the portion following the '?').
350      * @param search the new search portion of the location URL
351      * @throws Exception if an error occurs
352      * @see <a href="http://msdn.microsoft.com/en-us/library/ms534620.aspx">MSDN Documentation</a>
353      */
354     public void setSearch(final String search) throws Exception {
355         setUrl(UrlUtils.getUrlWithNewQuery(getUrl(), search));
356     }
357 
358     /**
359      * Returns the hash portion of the location URL (the portion following the '#').
360      * @return the hash portion of the location URL
361      * @see <a href="http://msdn.microsoft.com/en-us/library/ms533775.aspx">MSDN Documentation</a>
362      */
363     public String getHash() {
364         String hash = hash_;
365 
366         if (hash_ != null) {
367             hash = decodeHash(hash);
368         }
369 
370         if (StringUtils.isEmpty(hash)) {
371             // nothing to do
372         }
373         else {
374             return "#" + UrlUtils.encodeHash(hash);
375         }
376 
377         return "";
378     }
379 
380     private String getHash(final boolean encoded) {
381         if (hash_ == null || hash_.isEmpty()) {
382             return null;
383         }
384         if (encoded) {
385             return UrlUtils.encodeHash(hash_);
386         }
387         return hash_;
388     }
389 
390     /**
391      * Sets the hash portion of the location URL (the portion following the '#').
392      *
393      * @param hash the new hash portion of the location URL
394      * @see <a href="http://msdn.microsoft.com/en-us/library/ms533775.aspx">MSDN Documentation</a>
395      */
396     public void setHash(final String hash) {
397         // IMPORTANT: This method must not call setUrl(), because
398         // we must not hit the server just to change the hash!
399         setHash(getHref(), hash, true);
400     }
401 
402     /**
403      * Sets the hash portion of the location URL (the portion following the '#').
404      *
405      * @param oldURL the old URL
406      * @param hash the new hash portion of the location URL
407      */
408     public void setHash(final String oldURL, final String hash) {
409         setHash(oldURL, hash, true);
410     }
411 
412     /**
413      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
414      *
415      * Sets the hash portion of the location URL (the portion following the '#').
416      *
417      * @param oldURL the old URL
418      * @param hash the new hash portion of the location URL
419      * @param triggerHashChanged option to disable event triggering
420      */
421     public void setHash(final String oldURL, String hash, final boolean triggerHashChanged) {
422         // IMPORTANT: This method must not call setUrl(), because
423         // we must not hit the server just to change the hash!
424         if (hash != null && !hash.isEmpty() && hash.charAt(0) == '#') {
425             hash = hash.substring(1);
426         }
427         final boolean hasChanged = hash != null && !hash.equals(hash_);
428         hash_ = hash;
429 
430         if (triggerHashChanged && hasChanged) {
431             final Window w = getWindow();
432             final Event event = new HashChangeEvent(w, Event.TYPE_HASH_CHANGE, oldURL, getHref());
433             w.executeEventLocally(event);
434         }
435     }
436 
437     private static String decodeHash(final String hash) {
438         if (hash.indexOf('%') == -1) {
439             return hash;
440         }
441         return UrlUtils.decode(hash);
442     }
443 
444     /**
445      * Returns the hostname portion of the location URL.
446      * @return the hostname portion of the location URL
447      * @see <a href="http://msdn.microsoft.com/en-us/library/ms533785.aspx">MSDN Documentation</a>
448      */
449     public String getHostname() {
450         return getUrl().getHost();
451     }
452 
453     /**
454      * Sets the hostname portion of the location URL.
455      * @param hostname the new hostname portion of the location URL
456      * @throws Exception if an error occurs
457      * @see <a href="http://msdn.microsoft.com/en-us/library/ms533785.aspx">MSDN Documentation</a>
458      */
459     public void setHostname(final String hostname) throws Exception {
460         setUrl(UrlUtils.getUrlWithNewHost(getUrl(), hostname));
461     }
462 
463     /**
464      * Returns the host portion of the location URL (the '[hostname]:[port]' portion).
465      * @return the host portion of the location URL
466      * @see <a href="http://msdn.microsoft.com/en-us/library/ms533784.aspx">MSDN Documentation</a>
467      */
468     public String getHost() {
469         final URL url = getUrl();
470         final int port = url.getPort();
471         final String host = url.getHost();
472 
473         if (port == -1) {
474             return host;
475         }
476         return host + ":" + port;
477     }
478 
479     /**
480      * Sets the host portion of the location URL (the '[hostname]:[port]' portion).
481      * @param host the new host portion of the location URL
482      * @throws Exception if an error occurs
483      * @see <a href="http://msdn.microsoft.com/en-us/library/ms533784.aspx">MSDN Documentation</a>
484      */
485     public void setHost(final String host) throws Exception {
486         final String hostname;
487         final int port;
488         final int index = host.indexOf(':');
489         if (index == -1) {
490             hostname = host;
491             port = -1;
492         }
493         else {
494             hostname = host.substring(0, index);
495             port = Integer.parseInt(host.substring(index + 1));
496         }
497         final URL url = UrlUtils.getUrlWithNewHostAndPort(getUrl(), hostname, port);
498         setUrl(url);
499     }
500 
501     /**
502      * Returns the pathname portion of the location URL.
503      * @return the pathname portion of the location URL
504      * @see <a href="http://msdn.microsoft.com/en-us/library/ms534332.aspx">MSDN Documentation</a>
505      */
506     public String getPathname() {
507         if (UrlUtils.URL_ABOUT_BLANK == getUrl()) {
508             return "blank";
509         }
510         return getUrl().getPath();
511     }
512 
513     /**
514      * Sets the pathname portion of the location URL.
515      * @param pathname the new pathname portion of the location URL
516      * @throws Exception if an error occurs
517      * @see <a href="http://msdn.microsoft.com/en-us/library/ms534332.aspx">MSDN Documentation</a>
518      */
519     public void setPathname(final String pathname) throws Exception {
520         setUrl(UrlUtils.getUrlWithNewPath(getUrl(), pathname));
521     }
522 
523     /**
524      * Returns the port portion of the location URL.
525      * @return the port portion of the location URL
526      * @see <a href="http://msdn.microsoft.com/en-us/library/ms534342.aspx">MSDN Documentation</a>
527      */
528     public String getPort() {
529         final int port = getUrl().getPort();
530         if (port == -1) {
531             return "";
532         }
533         return Integer.toString(port);
534     }
535 
536     /**
537      * Sets the port portion of the location URL.
538      * @param port the new port portion of the location URL
539      * @throws Exception if an error occurs
540      * @see <a href="http://msdn.microsoft.com/en-us/library/ms534342.aspx">MSDN Documentation</a>
541      */
542     public void setPort(final String port) throws Exception {
543         setUrl(UrlUtils.getUrlWithNewPort(getUrl(), Integer.parseInt(port)));
544     }
545 
546     /**
547      * Returns the protocol portion of the location URL, including the trailing ':'.
548      * @return the protocol portion of the location URL, including the trailing ':'
549      * @see <a href="http://msdn.microsoft.com/en-us/library/ms534353.aspx">MSDN Documentation</a>
550      */
551     public String getProtocol() {
552         return getUrl().getProtocol() + ":";
553     }
554 
555     /**
556      * Sets the protocol portion of the location URL.
557      * @param protocol the new protocol portion of the location URL
558      * @throws Exception if an error occurs
559      * @see <a href="http://msdn.microsoft.com/en-us/library/ms534353.aspx">MSDN Documentation</a>
560      */
561     public void setProtocol(final String protocol) throws Exception {
562         setUrl(UrlUtils.getUrlWithNewProtocol(getUrl(), protocol));
563     }
564 
565     /**
566      * Returns this location's current URL.
567      * @return this location's current URL
568      */
569     private URL getUrl() {
570         return window_.getWebWindow().getEnclosedPage().getUrl();
571     }
572 
573     /**
574      * Sets this location's URL, triggering a server hit and loading the resultant document
575      * into this location's window.
576      * @param url This location's new URL
577      * @throws IOException if there is a problem loading the new location
578      */
579     private void setUrl(final URL url) throws IOException {
580         final WebWindow webWindow = window_.getWebWindow();
581         final BrowserVersion browserVersion = webWindow.getWebClient().getBrowserVersion();
582 
583         final WebRequest webRequest = new WebRequest(url,
584                 browserVersion.getHtmlAcceptHeader(), browserVersion.getAcceptEncodingHeader());
585         webRequest.setRefererHeader(getUrl());
586 
587         webWindow.getWebClient().getPage(webWindow, webRequest);
588     }
589 
590     /**
591      * Returns the {@code origin} property.
592      * @return the {@code origin} property
593      */
594     public String getOrigin() {
595         return getUrl().getProtocol() + "://" + getHost();
596     }
597 }