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