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