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.logging.Log;
26  import org.apache.commons.logging.LogFactory;
27  import org.htmlunit.BrowserVersion;
28  import org.htmlunit.Page;
29  import org.htmlunit.WebRequest;
30  import org.htmlunit.WebWindow;
31  import org.htmlunit.corejs.javascript.FunctionObject;
32  import org.htmlunit.corejs.javascript.ScriptableObject;
33  import org.htmlunit.html.HtmlPage;
34  import org.htmlunit.javascript.HtmlUnitScriptable;
35  import org.htmlunit.javascript.configuration.JsxClass;
36  import org.htmlunit.javascript.configuration.JsxConstructor;
37  import org.htmlunit.javascript.host.event.Event;
38  import org.htmlunit.javascript.host.event.HashChangeEvent;
39  import org.htmlunit.protocol.javascript.JavaScriptURLConnection;
40  import org.htmlunit.util.StringUtils;
41  import org.htmlunit.util.UrlUtils;
42  
43  /**
44   * A JavaScript object for {@code Location}.
45   *
46   * @author Mike Bowler
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, 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.isEmptyOrNull(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, 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.isEmptyOrNull(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         if (StringUtils.isEmptyOrNull(hash_)) {
365             return "";
366         }
367 
368         if (hash_.indexOf('%') == -1) {
369             return "#" + UrlUtils.encodeHash(hash_);
370         }
371         return "#" + UrlUtils.encodeHash(UrlUtils.decode(hash_));
372     }
373 
374     private String getHash(final boolean encoded) {
375         if (hash_ == null || hash_.isEmpty()) {
376             return null;
377         }
378         if (encoded) {
379             return UrlUtils.encodeHash(hash_);
380         }
381         return hash_;
382     }
383 
384     /**
385      * Sets the hash portion of the location URL (the portion following the '#').
386      *
387      * @param hash the new hash portion of the location URL
388      * @see <a href="http://msdn.microsoft.com/en-us/library/ms533775.aspx">MSDN Documentation</a>
389      */
390     public void setHash(final String hash) {
391         // IMPORTANT: This method must not call setUrl(), because
392         // we must not hit the server just to change the hash!
393         setHash(getHref(), hash, true);
394     }
395 
396     /**
397      * Sets the hash portion of the location URL (the portion following the '#').
398      *
399      * @param oldURL the old URL
400      * @param hash the new hash portion of the location URL
401      */
402     public void setHash(final String oldURL, final String hash) {
403         setHash(oldURL, hash, true);
404     }
405 
406     /**
407      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
408      *
409      * Sets the hash portion of the location URL (the portion following the '#').
410      *
411      * @param oldURL the old URL
412      * @param hash the new hash portion of the location URL
413      * @param triggerHashChanged option to disable event triggering
414      */
415     public void setHash(final String oldURL, String hash, final boolean triggerHashChanged) {
416         // IMPORTANT: This method must not call setUrl(), because
417         // we must not hit the server just to change the hash!
418         if (hash != null && !hash.isEmpty() && hash.charAt(0) == '#') {
419             hash = hash.substring(1);
420         }
421         final boolean hasChanged = hash != null && !hash.equals(hash_);
422         hash_ = hash;
423 
424         if (triggerHashChanged && hasChanged) {
425             final Window w = getWindow();
426             final Event event = new HashChangeEvent(w, Event.TYPE_HASH_CHANGE, oldURL, getHref());
427             w.executeEventLocally(event);
428         }
429     }
430 
431     /**
432      * Returns the hostname portion of the location URL.
433      * @return the hostname portion of the location URL
434      * @see <a href="http://msdn.microsoft.com/en-us/library/ms533785.aspx">MSDN Documentation</a>
435      */
436     public String getHostname() {
437         return getUrl().getHost();
438     }
439 
440     /**
441      * Sets the hostname portion of the location URL.
442      * @param hostname the new hostname portion of the location URL
443      * @throws Exception if an error occurs
444      * @see <a href="http://msdn.microsoft.com/en-us/library/ms533785.aspx">MSDN Documentation</a>
445      */
446     public void setHostname(final String hostname) throws Exception {
447         setUrl(UrlUtils.getUrlWithNewHost(getUrl(), hostname));
448     }
449 
450     /**
451      * Returns the host portion of the location URL (the '[hostname]:[port]' portion).
452      * @return the host portion of the location URL
453      * @see <a href="http://msdn.microsoft.com/en-us/library/ms533784.aspx">MSDN Documentation</a>
454      */
455     public String getHost() {
456         final URL url = getUrl();
457         final int port = url.getPort();
458         final String host = url.getHost();
459 
460         if (port == -1) {
461             return host;
462         }
463         return host + ":" + port;
464     }
465 
466     /**
467      * Sets the host portion of the location URL (the '[hostname]:[port]' portion).
468      * @param host the new host portion of the location URL
469      * @throws Exception if an error occurs
470      * @see <a href="http://msdn.microsoft.com/en-us/library/ms533784.aspx">MSDN Documentation</a>
471      */
472     public void setHost(final String host) throws Exception {
473         final String hostname;
474         final int port;
475         final int index = host.indexOf(':');
476         if (index == -1) {
477             hostname = host;
478             port = -1;
479         }
480         else {
481             hostname = host.substring(0, index);
482             port = Integer.parseInt(host.substring(index + 1));
483         }
484         final URL url = UrlUtils.getUrlWithNewHostAndPort(getUrl(), hostname, port);
485         setUrl(url);
486     }
487 
488     /**
489      * Returns the pathname portion of the location URL.
490      * @return the pathname portion of the location URL
491      * @see <a href="http://msdn.microsoft.com/en-us/library/ms534332.aspx">MSDN Documentation</a>
492      */
493     public String getPathname() {
494         if (UrlUtils.URL_ABOUT_BLANK == getUrl()) {
495             return "blank";
496         }
497         return getUrl().getPath();
498     }
499 
500     /**
501      * Sets the pathname portion of the location URL.
502      * @param pathname the new pathname portion of the location URL
503      * @throws Exception if an error occurs
504      * @see <a href="http://msdn.microsoft.com/en-us/library/ms534332.aspx">MSDN Documentation</a>
505      */
506     public void setPathname(final String pathname) throws Exception {
507         setUrl(UrlUtils.getUrlWithNewPath(getUrl(), pathname));
508     }
509 
510     /**
511      * Returns the port portion of the location URL.
512      * @return the port portion of the location URL
513      * @see <a href="http://msdn.microsoft.com/en-us/library/ms534342.aspx">MSDN Documentation</a>
514      */
515     public String getPort() {
516         final int port = getUrl().getPort();
517         if (port == -1) {
518             return "";
519         }
520         return Integer.toString(port);
521     }
522 
523     /**
524      * Sets the port portion of the location URL.
525      * @param port the new port portion of the location URL
526      * @throws Exception if an error occurs
527      * @see <a href="http://msdn.microsoft.com/en-us/library/ms534342.aspx">MSDN Documentation</a>
528      */
529     public void setPort(final String port) throws Exception {
530         setUrl(UrlUtils.getUrlWithNewPort(getUrl(), Integer.parseInt(port)));
531     }
532 
533     /**
534      * Returns the protocol portion of the location URL, including the trailing ':'.
535      * @return the protocol portion of the location URL, including the trailing ':'
536      * @see <a href="http://msdn.microsoft.com/en-us/library/ms534353.aspx">MSDN Documentation</a>
537      */
538     public String getProtocol() {
539         return getUrl().getProtocol() + ":";
540     }
541 
542     /**
543      * Sets the protocol portion of the location URL.
544      * @param protocol the new protocol portion of the location URL
545      * @throws Exception if an error occurs
546      * @see <a href="http://msdn.microsoft.com/en-us/library/ms534353.aspx">MSDN Documentation</a>
547      */
548     public void setProtocol(final String protocol) throws Exception {
549         setUrl(UrlUtils.getUrlWithNewProtocol(getUrl(), protocol));
550     }
551 
552     /**
553      * Returns this location's current URL.
554      * @return this location's current URL
555      */
556     private URL getUrl() {
557         return window_.getWebWindow().getEnclosedPage().getUrl();
558     }
559 
560     /**
561      * Sets this location's URL, triggering a server hit and loading the resultant document
562      * into this location's window.
563      * @param url This location's new URL
564      * @throws IOException if there is a problem loading the new location
565      */
566     private void setUrl(final URL url) throws IOException {
567         final WebWindow webWindow = window_.getWebWindow();
568         final BrowserVersion browserVersion = webWindow.getWebClient().getBrowserVersion();
569 
570         final WebRequest webRequest = new WebRequest(url,
571                 browserVersion.getHtmlAcceptHeader(), browserVersion.getAcceptEncodingHeader());
572         webRequest.setRefererHeader(getUrl());
573 
574         webWindow.getWebClient().getPage(webWindow, webRequest);
575     }
576 
577     /**
578      * Returns the {@code origin} property.
579      * @return the {@code origin} property
580      */
581     public String getOrigin() {
582         return getUrl().getProtocol() + "://" + getHost();
583     }
584 }