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_RELOAD_REFERRER;
18  
19  import java.io.IOException;
20  import java.lang.reflect.Method;
21  import java.net.MalformedURLException;
22  import java.net.URL;
23  
24  import org.apache.commons.logging.Log;
25  import org.apache.commons.logging.LogFactory;
26  import org.htmlunit.BrowserVersion;
27  import org.htmlunit.Page;
28  import org.htmlunit.WebRequest;
29  import org.htmlunit.WebWindow;
30  import org.htmlunit.corejs.javascript.Context;
31  import org.htmlunit.corejs.javascript.Function;
32  import org.htmlunit.corejs.javascript.FunctionObject;
33  import org.htmlunit.corejs.javascript.ScriptableObject;
34  import org.htmlunit.corejs.javascript.VarScope;
35  import org.htmlunit.html.HtmlPage;
36  import org.htmlunit.javascript.HtmlUnitScriptable;
37  import org.htmlunit.javascript.JavaScriptEngine;
38  import org.htmlunit.javascript.configuration.JsxClass;
39  import org.htmlunit.javascript.configuration.JsxConstructor;
40  import org.htmlunit.javascript.host.event.Event;
41  import org.htmlunit.javascript.host.event.HashChangeEvent;
42  import org.htmlunit.protocol.javascript.JavaScriptURLConnection;
43  import org.htmlunit.util.StringUtils;
44  import org.htmlunit.util.UrlUtils;
45  
46  /**
47   * JavaScript host object for {@code Location}.
48   *
49   * @author Mike Bowler
50   * @author Michael Ottati
51   * @author Marc Guillemot
52   * @author Chris Erskine
53   * @author Daniel Gredler
54   * @author David K. Taylor
55   * @author Ahmed Ashour
56   * @author Ronald Brill
57   * @author Frank Danek
58   * @author Adam Afeltowicz
59   * @author Atsushi Nakagawa
60   * @author Lai Quang Duong
61   * @author Kanoko Yamamoto
62   *
63   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location">MDN Documentation</a>
64   */
65  @JsxClass
66  public class Location extends HtmlUnitScriptable {
67  
68      private static final Log LOG = LogFactory.getLog(Location.class);
69      private static final String UNKNOWN = "null";
70  
71      /**
72       * The window which owns this location object.
73       */
74      private Window window_;
75  
76      private static final Method METHOD_ASSIGN;
77      private static final Method METHOD_RELOAD;
78      private static final Method METHOD_REPLACE;
79      private static final Method METHOD_TO_STRING;
80  
81      private static final Method GETTER_HASH;
82      private static final Method SETTER_HASH;
83  
84      private static final Method GETTER_HOST;
85      private static final Method SETTER_HOST;
86  
87      private static final Method GETTER_HOSTNAME;
88      private static final Method SETTER_HOSTNAME;
89  
90      private static final Method GETTER_HREF;
91      private static final Method SETTER_HREF;
92  
93      private static final Method GETTER_ORIGIN;
94  
95      private static final Method GETTER_PATHNAME;
96      private static final Method SETTER_PATHNAME;
97  
98      private static final Method GETTER_PORT;
99      private static final Method SETTER_PORT;
100 
101     private static final Method GETTER_PROTOCOL;
102     private static final Method SETTER_PROTOCOL;
103 
104     private static final Method GETTER_SEARCH;
105     private static final Method SETTER_SEARCH;
106 
107     static {
108         try {
109             METHOD_ASSIGN = Location.class.getDeclaredMethod("assign", String.class);
110             METHOD_RELOAD = Location.class.getDeclaredMethod("reload", boolean.class);
111             METHOD_REPLACE = Location.class.getDeclaredMethod("replace", String.class);
112             METHOD_TO_STRING = Location.class.getDeclaredMethod("jsToString");
113 
114             GETTER_HASH = Location.class.getDeclaredMethod("getHash");
115             SETTER_HASH = Location.class.getDeclaredMethod("setHash", String.class);
116 
117             GETTER_HOST = Location.class.getDeclaredMethod("getHost");
118             SETTER_HOST = Location.class.getDeclaredMethod("setHost", String.class);
119 
120             GETTER_HOSTNAME = Location.class.getDeclaredMethod("getHostname");
121             SETTER_HOSTNAME = Location.class.getDeclaredMethod("setHostname", String.class);
122 
123             GETTER_HREF = Location.class.getDeclaredMethod("getHref");
124             SETTER_HREF = Location.class.getDeclaredMethod("setHref", String.class);
125 
126             GETTER_ORIGIN = Location.class.getDeclaredMethod("getOrigin");
127 
128             GETTER_PATHNAME = Location.class.getDeclaredMethod("getPathname");
129             SETTER_PATHNAME = Location.class.getDeclaredMethod("setPathname", String.class);
130 
131             GETTER_PORT = Location.class.getDeclaredMethod("getPort");
132             SETTER_PORT = Location.class.getDeclaredMethod("setPort", String.class);
133 
134             GETTER_PROTOCOL = Location.class.getDeclaredMethod("getProtocol");
135             SETTER_PROTOCOL = Location.class.getDeclaredMethod("setProtocol", String.class);
136 
137             GETTER_SEARCH = Location.class.getDeclaredMethod("getSearch");
138             SETTER_SEARCH = Location.class.getDeclaredMethod("setSearch", String.class);
139         }
140         catch (NoSuchMethodException | SecurityException e) {
141             throw new RuntimeException(e);
142         }
143     }
144 
145     /**
146      * The current hash; cached locally to avoid modifying the page's URL and
147      * forcing a page reload each time this changes.
148      */
149     private String hash_;
150 
151     /**
152      * Creates an instance of this object.
153      *
154      * @param cx the current context
155      * @param scope the scope
156      * @param args the constructor arguments
157      * @param ctorObj the function object
158      * @param inNewExpr whether invoked via {@code new}
159      * @return the new {@code Location} instance
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 {@code 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      *
228      * @param url the URL of the new HTML document to load
229      * @throws IOException if loading the specified location fails
230      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/assign">MDN Documentation</a>
231      */
232     public void assign(final String url) throws IOException {
233         setHref(url);
234     }
235 
236     /**
237      * Reloads the current page, optionally forcing retrieval from the server even if
238      * the browser cache contains the latest version of the document.
239      *
240      * @param force if {@code true}, force reload from the server; otherwise, the browser may reload from cache
241      * @throws IOException if there is a problem reloading the page
242      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/reload">MDN Documentation</a>
243      */
244     public void reload(final boolean force) throws IOException {
245         final WebWindow webWindow = window_.getWebWindow();
246         final HtmlPage htmlPage = (HtmlPage) webWindow.getEnclosedPage();
247         final WebRequest request = htmlPage.getWebResponse().getWebRequest();
248 
249         // update request url with location.href in case hash was changed
250         request.setUrl(new URL(getHref()));
251         if (getBrowserVersion().hasFeature(JS_LOCATION_RELOAD_REFERRER)) {
252             request.setRefererHeader(htmlPage.getUrl());
253         }
254 
255         webWindow.getWebClient().download(webWindow, "", request, false, null, "JS location.reload");
256     }
257 
258     /**
259      * Loads the document at the specified URL, replacing the current entry in the session history
260      * so that the previous page cannot be reached via the back button.
261      *
262      * @param url the new URL to load
263      * @throws IOException if loading the specified location fails
264      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/replace">MDN Documentation</a>
265      */
266     public void replace(final String url) throws IOException {
267         window_.getWebWindow().getHistory().removeCurrent();
268         setHref(url);
269     }
270 
271     /**
272      * Returns the location URL as a string.
273      *
274      * @return the location URL
275      */
276     public String jsToString() {
277         if (window_ != null) {
278             return getHref();
279         }
280         return "";
281     }
282 
283     /**
284      * Returns the full URL of the current page.
285      *
286      * @return the location URL
287      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/href">MDN Documentation</a>
288      */
289     public String getHref() {
290         final WebWindow webWindow = window_.getWebWindow();
291         final Page page = webWindow.getEnclosedPage();
292         if (page == null) {
293             return UNKNOWN;
294         }
295         try {
296             URL url = page.getUrl();
297             final String hash = getHash(true);
298             if (hash != null) {
299                 url = UrlUtils.getUrlWithNewRef(url, hash);
300             }
301             String s = url.toExternalForm();
302             if (s.startsWith("file:/") && !s.startsWith("file:///")) {
303                 // Java (sometimes?) returns file URLs with a single slash; however, browsers return
304                 // three slashes. See http://www.cyanwerks.com/file-url-formats.html for more info.
305                 s = "file:///" + s.substring("file:/".length());
306             }
307             return s;
308         }
309         catch (final MalformedURLException e) {
310             if (LOG.isErrorEnabled()) {
311                 LOG.error(e.getMessage(), e);
312             }
313             return page.getUrl().toExternalForm();
314         }
315     }
316 
317     /**
318      * Sets the location URL to an entirely new value, navigating to the new page.
319      *
320      * @param newLocation the new location URL
321      * @throws IOException if loading the specified location fails
322      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/href">MDN Documentation</a>
323      */
324     public void setHref(final String newLocation) throws IOException {
325         if (newLocation.startsWith(JavaScriptURLConnection.JAVASCRIPT_PREFIX)) {
326             final String script = newLocation.substring(11);
327             final HtmlPage page = (HtmlPage) window_.getWebWindow().getEnclosedPage();
328             page.executeJavaScript(script, "new location value", 1);
329             return;
330         }
331 
332         try {
333             // we need to get the caller window to get the calling page
334             WebWindow webWindow = ((Window) JavaScriptEngine.getTopCallScope().getGlobalThis()).getWebWindow();
335             final HtmlPage callingPage = (HtmlPage) webWindow.getEnclosedPage();
336 
337             final BrowserVersion browserVersion = webWindow.getWebClient().getBrowserVersion();
338 
339             URL url = callingPage.getFullyQualifiedUrl(newLocation);
340             // fix for empty url
341             if (StringUtils.isEmptyOrNull(newLocation)) {
342                 url = UrlUtils.getUrlWithNewRef(url, null);
343             }
344 
345             final WebRequest webRequest = new WebRequest(url,
346                         browserVersion.getHtmlAcceptHeader(), browserVersion.getAcceptEncodingHeader());
347             webRequest.setRefererHeader(callingPage.getUrl());
348 
349             webRequest.markAsNavigation(getUrl(), false);
350             webRequest.setFetchModeOverride(WebRequest.FetchMode.NAVIGATE);
351 
352             webWindow = window_.getWebWindow();
353             webWindow.getWebClient().download(webWindow, "", webRequest, true, null, "JS set location");
354         }
355         catch (final MalformedURLException e) {
356             if (LOG.isErrorEnabled()) {
357                 LOG.error("setHref('" + newLocation + "') got MalformedURLException", e);
358             }
359             throw e;
360         }
361     }
362 
363     /**
364      * Returns the search portion of the location URL (the portion following the {@code ?}).
365      *
366      * @return the search portion of the location URL
367      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/search">MDN Documentation</a>
368      */
369     public String getSearch() {
370         final URL url = getUrl();
371         final String search = url.getQuery();
372         if (StringUtils.isEmptyOrNull(search)) {
373             return "";
374         }
375         return "?" + search;
376     }
377 
378     /**
379      * Sets the search portion of the location URL (the portion following the {@code ?}).
380      *
381      * @param search the new search portion of the location URL
382      * @throws Exception if an error occurs
383      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/search">MDN Documentation</a>
384      */
385     public void setSearch(final String search) throws Exception {
386         setUrl(UrlUtils.getUrlWithNewQuery(getUrl(), search));
387     }
388 
389     /**
390      * Returns the hash portion of the location URL (the portion following the {@code #}).
391      *
392      * @return the hash portion of the location URL
393      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/hash">MDN Documentation</a>
394      */
395     public String getHash() {
396         if (StringUtils.isEmptyOrNull(hash_)) {
397             return "";
398         }
399 
400         if (hash_.indexOf('%') == -1) {
401             return "#" + UrlUtils.encodeHash(hash_);
402         }
403         return "#" + UrlUtils.encodeHash(UrlUtils.decode(hash_));
404     }
405 
406     private String getHash(final boolean encoded) {
407         if (hash_ == null || hash_.isEmpty()) {
408             return null;
409         }
410         if (encoded) {
411             return UrlUtils.encodeHash(hash_);
412         }
413         return hash_;
414     }
415 
416     /**
417      * Sets the hash portion of the location URL (the portion following the {@code #}).
418      * This method does not cause a server hit.
419      *
420      * @param hash the new hash portion of the location URL
421      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/hash">MDN Documentation</a>
422      */
423     public void setHash(final String hash) {
424         // IMPORTANT: This method must not call setUrl(), because
425         // we must not hit the server just to change the hash!
426         setHash(getHref(), hash, true);
427     }
428 
429     /**
430      * Sets the hash portion of the location URL (the portion following the {@code #}).
431      *
432      * @param oldURL the old URL
433      * @param hash the new hash portion of the location URL
434      */
435     public void setHash(final String oldURL, final String hash) {
436         setHash(oldURL, hash, true);
437     }
438 
439     /**
440      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
441      *
442      * Sets the hash portion of the location URL (the portion following the {@code #}).
443      *
444      * @param oldURL the old URL
445      * @param hash the new hash portion of the location URL
446      * @param triggerHashChanged option to disable event triggering
447      */
448     public void setHash(final String oldURL, String hash, final boolean triggerHashChanged) {
449         // IMPORTANT: This method must not call setUrl(), because
450         // we must not hit the server just to change the hash!
451         if (hash != null && !hash.isEmpty() && hash.charAt(0) == '#') {
452             hash = hash.substring(1);
453         }
454         final boolean hasChanged = hash != null && !hash.equals(hash_);
455         hash_ = hash;
456 
457         if (hasChanged) {
458             final Window w = getWindow();
459             final Page page = w.getWebWindow().getEnclosedPage();
460             if (page != null) {
461                 final WebRequest request = page.getWebResponse().getWebRequest();
462                 request.setUrl(UrlUtils.toUrlSafe(getHref()));
463             }
464 
465             if (triggerHashChanged) {
466                 final Event event = new HashChangeEvent(w, Event.TYPE_HASH_CHANGE, oldURL, getHref());
467                 w.executeEventLocally(event);
468             }
469         }
470     }
471 
472     /**
473      * Returns the hostname portion of the location URL.
474      *
475      * @return the hostname portion of the location URL
476      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/hostname">MDN Documentation</a>
477      */
478     public String getHostname() {
479         return getUrl().getHost();
480     }
481 
482     /**
483      * Sets the hostname portion of the location URL.
484      *
485      * @param hostname the new hostname portion of the location URL
486      * @throws Exception if an error occurs
487      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/hostname">MDN Documentation</a>
488      */
489     public void setHostname(final String hostname) throws Exception {
490         setUrl(UrlUtils.getUrlWithNewHost(getUrl(), hostname));
491     }
492 
493     /**
494      * Returns the host portion of the location URL (the {@code [hostname]:[port]} portion).
495      *
496      * @return the host portion of the location URL
497      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/host">MDN Documentation</a>
498      */
499     public String getHost() {
500         final URL url = getUrl();
501         final int port = url.getPort();
502         final String host = url.getHost();
503 
504         if (port == -1) {
505             return host;
506         }
507         return host + ":" + port;
508     }
509 
510     /**
511      * Sets the host portion of the location URL (the {@code [hostname]:[port]} portion).
512      *
513      * @param host the new host portion of the location URL
514      * @throws Exception if an error occurs
515      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/host">MDN Documentation</a>
516      */
517     public void setHost(final String host) throws Exception {
518         final String hostname;
519         final int port;
520         final int index = host.indexOf(':');
521         if (index == -1) {
522             hostname = host;
523             port = -1;
524         }
525         else {
526             hostname = host.substring(0, index);
527             port = Integer.parseInt(host.substring(index + 1));
528         }
529         final URL url = UrlUtils.getUrlWithNewHostAndPort(getUrl(), hostname, port);
530         setUrl(url);
531     }
532 
533     /**
534      * Returns the pathname portion of the location URL.
535      *
536      * @return the pathname portion of the location URL
537      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/pathname">MDN Documentation</a>
538      */
539     public String getPathname() {
540         if (UrlUtils.URL_ABOUT_BLANK == getUrl()) {
541             return "blank";
542         }
543         return getUrl().getPath();
544     }
545 
546     /**
547      * Sets the pathname portion of the location URL.
548      *
549      * @param pathname the new pathname portion of the location URL
550      * @throws Exception if an error occurs
551      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/pathname">MDN Documentation</a>
552      */
553     public void setPathname(final String pathname) throws Exception {
554         setUrl(UrlUtils.getUrlWithNewPath(getUrl(), pathname));
555     }
556 
557     /**
558      * Returns the port portion of the location URL.
559      *
560      * @return the port portion of the location URL, or an empty string if no port is specified
561      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/port">MDN Documentation</a>
562      */
563     public String getPort() {
564         final int port = getUrl().getPort();
565         if (port == -1) {
566             return "";
567         }
568         return Integer.toString(port);
569     }
570 
571     /**
572      * Sets the port portion of the location URL.
573      *
574      * @param port the new port portion of the location URL
575      * @throws Exception if an error occurs
576      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/port">MDN Documentation</a>
577      */
578     public void setPort(final String port) throws Exception {
579         setUrl(UrlUtils.getUrlWithNewPort(getUrl(), Integer.parseInt(port)));
580     }
581 
582     /**
583      * Returns the protocol portion of the location URL, including the trailing {@code :}.
584      *
585      * @return the protocol portion of the location URL
586      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/protocol">MDN Documentation</a>
587      */
588     public String getProtocol() {
589         return getUrl().getProtocol() + ":";
590     }
591 
592     /**
593      * Sets the protocol portion of the location URL.
594      *
595      * @param protocol the new protocol portion of the location URL
596      * @throws Exception if an error occurs
597      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Location/protocol">MDN Documentation</a>
598      */
599     public void setProtocol(final String protocol) throws Exception {
600         setUrl(UrlUtils.getUrlWithNewProtocol(getUrl(), protocol));
601     }
602 
603     /**
604      * Returns this location's current URL.
605      *
606      * @return this location's current URL
607      */
608     private URL getUrl() {
609         return window_.getWebWindow().getEnclosedPage().getUrl();
610     }
611 
612     /**
613      * Sets this location's URL, triggering a server request and loading the resulting document
614      * into this location's window.
615      *
616      * @param url this location's new URL
617      * @throws IOException if there is a problem loading the new location
618      */
619     private void setUrl(final URL url) throws IOException {
620         final WebWindow webWindow = window_.getWebWindow();
621         final BrowserVersion browserVersion = webWindow.getWebClient().getBrowserVersion();
622 
623         final WebRequest webRequest = new WebRequest(url,
624                 browserVersion.getHtmlAcceptHeader(), browserVersion.getAcceptEncodingHeader());
625 
626         webRequest.markAsNavigation(getUrl(), false);
627         webRequest.setFetchModeOverride(WebRequest.FetchMode.NAVIGATE);
628 
629         webRequest.setRefererHeader(getUrl());
630 
631         webWindow.getWebClient().getPage(webWindow, webRequest);
632     }
633 
634     /**
635      * Returns the {@code origin} property.
636      *
637      * @return the {@code origin} property
638      */
639     public String getOrigin() {
640         return getUrl().getProtocol() + "://" + getHost();
641     }
642 }