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.html;
16  
17  import static org.htmlunit.BrowserVersionFeatures.ANCHOR_SEND_PING_REQUEST;
18  
19  import java.io.IOException;
20  import java.net.MalformedURLException;
21  import java.net.URL;
22  import java.util.Locale;
23  import java.util.Map;
24  
25  import org.apache.commons.lang3.ArrayUtils;
26  import org.apache.commons.lang3.StringUtils;
27  import org.apache.commons.lang3.Strings;
28  import org.apache.commons.logging.Log;
29  import org.apache.commons.logging.LogFactory;
30  import org.htmlunit.BrowserVersion;
31  import org.htmlunit.HttpHeader;
32  import org.htmlunit.HttpMethod;
33  import org.htmlunit.Page;
34  import org.htmlunit.SgmlPage;
35  import org.htmlunit.WebClient;
36  import org.htmlunit.WebRequest;
37  import org.htmlunit.WebWindow;
38  import org.htmlunit.javascript.host.event.Event;
39  import org.htmlunit.javascript.host.html.HTMLElement;
40  import org.htmlunit.protocol.javascript.JavaScriptURLConnection;
41  import org.htmlunit.util.UrlUtils;
42  
43  /**
44   * Wrapper for the HTML element "a".
45   *
46   * @author Mike Bowler
47   * @author David K. Taylor
48   * @author Christian Sell
49   * @author Ahmed Ashour
50   * @author Dmitri Zoubkov
51   * @author Ronald Brill
52   * @author Frank Danek
53   * @author Lai Quang Duong
54   */
55  public class HtmlAnchor extends HtmlElement {
56  
57      private static final Log LOG = LogFactory.getLog(HtmlAnchor.class);
58  
59      /** The HTML tag represented by this element. */
60      public static final String TAG_NAME = "a";
61  
62      /**
63       * Creates a new instance.
64       *
65       * @param qualifiedName the qualified name of the element type to instantiate
66       * @param page the page that contains this element
67       * @param attributes the initial attributes
68       */
69      HtmlAnchor(final String qualifiedName, final SgmlPage page,
70              final Map<String, DomAttr> attributes) {
71          super(qualifiedName, page, attributes);
72      }
73  
74      /**
75       * {@inheritDoc}
76       */
77      @Override
78      @SuppressWarnings("unchecked")
79      public <P extends Page> P click(final Event event,
80              final boolean shiftKey, final boolean ctrlKey, final boolean altKey,
81              final boolean ignoreVisibility) throws IOException {
82          WebWindow oldWebWindow = null;
83          if (ctrlKey) {
84              oldWebWindow = ((HTMLElement) event.getSrcElement()).getDomNodeOrDie()
85                      .getPage().getWebClient().getCurrentWindow();
86          }
87  
88          P page = super.click(event, shiftKey, ctrlKey, altKey, ignoreVisibility);
89  
90          if (ctrlKey) {
91              page.getEnclosingWindow().getWebClient().setCurrentWindow(oldWebWindow);
92              page = (P) oldWebWindow.getEnclosedPage();
93          }
94  
95          return page;
96      }
97  
98      /**
99       * Same as {@link #doClickStateUpdate(boolean, boolean)}, except that it accepts an {@code href} suffix,
100      * needed when a click is performed on an image map to pass information on the click position.
101      *
102      * @param shiftKey {@code true} if SHIFT is pressed
103      * @param ctrlKey {@code true} if CTRL is pressed
104      * @param hrefSuffix the suffix to add to the anchor's {@code href} attribute
105      *        (for instance coordinates from an image map)
106      * @throws IOException if an IO error occurs
107      */
108     protected void doClickStateUpdate(final boolean shiftKey, final boolean ctrlKey, final String hrefSuffix)
109             throws IOException {
110         final String href = (getHrefAttribute() + hrefSuffix).trim();
111         if (LOG.isDebugEnabled()) {
112             final String w = getPage().getEnclosingWindow().getName();
113             LOG.debug("do click action in window '" + w + "', using href '" + href + "'");
114         }
115         if (ATTRIBUTE_NOT_DEFINED == getHrefAttribute()) {
116             return;
117         }
118         final String downloadAttribute = getDownloadAttribute();
119         HtmlPage page = (HtmlPage) getPage();
120         if (Strings.CI.startsWith(href, JavaScriptURLConnection.JAVASCRIPT_PREFIX)) {
121             final StringBuilder builder = new StringBuilder(href.length());
122             builder.append(JavaScriptURLConnection.JAVASCRIPT_PREFIX);
123             for (int i = JavaScriptURLConnection.JAVASCRIPT_PREFIX.length(); i < href.length(); i++) {
124                 final char ch = href.charAt(i);
125                 if (ch == '%' && i + 2 < href.length()) {
126                     final char ch1 = Character.toUpperCase(href.charAt(i + 1));
127                     final char ch2 = Character.toUpperCase(href.charAt(i + 2));
128                     if ((Character.isDigit(ch1) || ch1 >= 'A' && ch1 <= 'F')
129                             && (Character.isDigit(ch2) || ch2 >= 'A' && ch2 <= 'F')) {
130                         builder.append((char) Integer.parseInt(href.substring(i + 1, i + 3), 16));
131                         i += 2;
132                         continue;
133                     }
134                 }
135                 builder.append(ch);
136             }
137 
138             final String target;
139             if (shiftKey || ctrlKey || ATTRIBUTE_NOT_DEFINED != downloadAttribute) {
140                 target = WebClient.TARGET_BLANK;
141             }
142             else {
143                 target = page.getResolvedTarget(getTargetAttribute());
144             }
145             final WebWindow win = page.getWebClient().openTargetWindow(page.getEnclosingWindow(),
146                     target, WebClient.TARGET_SELF);
147             Page enclosedPage = win.getEnclosedPage();
148             if (enclosedPage == null) {
149                 win.getWebClient().getPage(win, WebRequest.newAboutBlankRequest());
150                 enclosedPage = win.getEnclosedPage();
151             }
152             if (enclosedPage != null && enclosedPage.isHtmlPage()) {
153                 page = (HtmlPage) enclosedPage;
154                 page.executeJavaScript(builder.toString(), "javascript url", getStartLineNumber());
155             }
156             return;
157         }
158 
159         final URL url = getTargetUrl(href, page);
160 
161         final WebClient webClient = page.getWebClient();
162         final BrowserVersion browser = webClient.getBrowserVersion();
163         if (ATTRIBUTE_NOT_DEFINED != getPingAttribute() && browser.hasFeature(ANCHOR_SEND_PING_REQUEST)) {
164             final URL pingUrl = getTargetUrl(getPingAttribute(), page);
165             final WebRequest pingRequest = new WebRequest(pingUrl, HttpMethod.POST);
166             pingRequest.setAdditionalHeader(HttpHeader.PING_FROM, page.getUrl().toExternalForm());
167             pingRequest.setAdditionalHeader(HttpHeader.PING_TO, url.toExternalForm());
168             pingRequest.setRequestBody("PING");
169             webClient.loadWebResponse(pingRequest);
170         }
171 
172         final WebRequest webRequest = new WebRequest(url, browser.getHtmlAcceptHeader(),
173                                                             browser.getAcceptEncodingHeader());
174         // use the page encoding even if this is a GET requests
175         webRequest.setCharset(page.getCharset());
176 
177         if (!relContainsNoreferrer()) {
178             webRequest.setRefererHeader(page.getUrl());
179         }
180 
181         if (LOG.isDebugEnabled()) {
182             LOG.debug(
183                     "Getting page for " + url.toExternalForm()
184                     + ", derived from href '" + href
185                     + "', using the originating URL "
186                     + page.getUrl());
187         }
188 
189         final String target;
190         if (shiftKey || ctrlKey
191                 || (webClient.getAttachmentHandler() == null
192                         && ATTRIBUTE_NOT_DEFINED != downloadAttribute)) {
193             target = WebClient.TARGET_BLANK;
194         }
195         else {
196             target = page.getResolvedTarget(getTargetAttribute());
197         }
198         page.getWebClient().download(page.getEnclosingWindow(), target, webRequest,
199                 true, (ATTRIBUTE_NOT_DEFINED != downloadAttribute) ? downloadAttribute : null, "Link click");
200     }
201 
202     private boolean relContainsNoreferrer() {
203         String rel = getRelAttribute();
204         if (rel != null) {
205             rel = rel.toLowerCase(Locale.ROOT);
206             return ArrayUtils.contains(org.htmlunit.util.StringUtils.splitAtBlank(rel), "noreferrer");
207         }
208         return false;
209     }
210 
211     /**
212      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
213      *
214      * @param href the href
215      * @param page the HtmlPage
216      * @return the calculated target url.
217      * @throws MalformedURLException if an IO error occurs
218      */
219     public static URL getTargetUrl(final String href, final HtmlPage page) throws MalformedURLException {
220         URL url = page.getFullyQualifiedUrl(href);
221         // fix for empty url
222         if (StringUtils.isEmpty(href)) {
223             url = UrlUtils.getUrlWithNewRef(url, null);
224         }
225         return url;
226     }
227 
228     /**
229      * {@inheritDoc}
230      */
231     @Override
232     protected boolean doClickStateUpdate(final boolean shiftKey, final boolean ctrlKey) throws IOException {
233         doClickStateUpdate(shiftKey, ctrlKey, "");
234         return false;
235     }
236 
237     /**
238      * Returns the value of the attribute {@code charset}. Refer to the
239      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
240      * documentation for details on the use of this attribute.
241      *
242      * @return the value of the attribute {@code charset} or an empty string if that attribute isn't defined
243      */
244     public final String getCharsetAttribute() {
245         return getAttributeDirect("charset");
246     }
247 
248     /**
249      * Returns the value of the attribute {@code type}. Refer to the
250      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
251      * documentation for details on the use of this attribute.
252      *
253      * @return the value of the attribute {@code type} or an empty string if that attribute isn't defined
254      */
255     public final String getTypeAttribute() {
256         return getAttributeDirect(TYPE_ATTRIBUTE);
257     }
258 
259     /**
260      * Returns the value of the attribute {@code name}. Refer to the
261      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
262      * documentation for details on the use of this attribute.
263      *
264      * @return the value of the attribute {@code name} or an empty string if that attribute isn't defined
265      */
266     public final String getNameAttribute() {
267         return getAttributeDirect(NAME_ATTRIBUTE);
268     }
269 
270     /**
271      * Returns the value of the attribute {@code href}. Refer to the
272      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
273      * documentation for details on the use of this attribute.
274      *
275      * @return the value of the attribute {@code href} or an empty string if that attribute isn't defined
276      */
277     public final String getHrefAttribute() {
278         return getAttributeDirect("href").trim();
279     }
280 
281     /**
282      * Returns the value of the attribute {@code hreflang}. Refer to the
283      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
284      * documentation for details on the use of this attribute.
285      *
286      * @return the value of the attribute {@code hreflang} or an empty string if that attribute isn't defined
287      */
288     public final String getHrefLangAttribute() {
289         return getAttributeDirect("hreflang");
290     }
291 
292     /**
293      * Returns the value of the attribute {@code rel}. Refer to the
294      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
295      * documentation for details on the use of this attribute.
296      *
297      * @return the value of the attribute {@code rel} or an empty string if that attribute isn't defined
298      */
299     public final String getRelAttribute() {
300         return getAttributeDirect("rel");
301     }
302 
303     /**
304      * Returns the value of the attribute {@code rev}. Refer to the
305      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
306      * documentation for details on the use of this attribute.
307      *
308      * @return the value of the attribute {@code rev} or an empty string if that attribute isn't defined
309      */
310     public final String getRevAttribute() {
311         return getAttributeDirect("rev");
312     }
313 
314     /**
315      * Returns the value of the attribute {@code accesskey}. Refer to the
316      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
317      * documentation for details on the use of this attribute.
318      *
319      * @return the value of the attribute {@code accesskey} or an empty string if that attribute isn't defined
320      */
321     public final String getAccessKeyAttribute() {
322         return getAttributeDirect("accesskey");
323     }
324 
325     /**
326      * Returns the value of the attribute {@code shape}. Refer to the
327      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
328      * documentation for details on the use of this attribute.
329      *
330      * @return the value of the attribute {@code shape} or an empty string if that attribute isn't defined
331      */
332     public final String getShapeAttribute() {
333         return getAttributeDirect("shape");
334     }
335 
336     /**
337      * Returns the value of the attribute {@code coords}. Refer to the
338      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
339      * documentation for details on the use of this attribute.
340      *
341      * @return the value of the attribute {@code coords} or an empty string if that attribute isn't defined
342      */
343     public final String getCoordsAttribute() {
344         return getAttributeDirect("coords");
345     }
346 
347     /**
348      * Returns the value of the attribute {@code tabindex}. Refer to the
349      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
350      * documentation for details on the use of this attribute.
351      *
352      * @return the value of the attribute {@code tabindex} or an empty string if that attribute isn't defined
353      */
354     public final String getTabIndexAttribute() {
355         return getAttributeDirect("tabindex");
356     }
357 
358     /**
359      * Returns the value of the attribute {@code onfocus}. Refer to the
360      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
361      * documentation for details on the use of this attribute.
362      *
363      * @return the value of the attribute {@code onfocus} or an empty string if that attribute isn't defined
364      */
365     public final String getOnFocusAttribute() {
366         return getAttributeDirect("onfocus");
367     }
368 
369     /**
370      * Returns the value of the attribute {@code onblur}. Refer to the
371      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
372      * documentation for details on the use of this attribute.
373      *
374      * @return the value of the attribute {@code onblur} or an empty string if that attribute isn't defined
375      */
376     public final String getOnBlurAttribute() {
377         return getAttributeDirect("onblur");
378     }
379 
380     /**
381      * Returns the value of the attribute {@code target}. Refer to the
382      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
383      * documentation for details on the use of this attribute.
384      *
385      * @return the value of the attribute {@code target} or an empty string if that attribute isn't defined
386      */
387     public final String getTargetAttribute() {
388         return getAttributeDirect("target");
389     }
390 
391     /**
392      * Open this link in a new window, much as web browsers do when you shift-click a link or use the context
393      * menu to open in a new window.
394      * <p>
395      * It should be noted that even web browsers will sometimes not give the expected result when using this
396      * method of following links. Links that have no real href and rely on JavaScript to do their work will
397      * fail.
398      *
399      * @return the page opened by this link, nested in a new {@link org.htmlunit.TopLevelWindow}
400      * @throws MalformedURLException if the href could not be converted to a valid URL
401      */
402     public final Page openLinkInNewWindow() throws MalformedURLException {
403         final URL target = ((HtmlPage) getPage()).getFullyQualifiedUrl(getHrefAttribute());
404         final String windowName = "HtmlAnchor.openLinkInNewWindow() target";
405         final WebWindow newWindow = getPage().getWebClient().openWindow(target, windowName);
406         return newWindow.getEnclosedPage();
407     }
408 
409     @Override
410     protected boolean isEmptyXmlTagExpanded() {
411         return true;
412     }
413 
414     /**
415      * {@inheritDoc}
416      */
417     @Override
418     public DisplayStyle getDefaultStyleDisplay() {
419         return DisplayStyle.INLINE;
420     }
421 
422     /**
423      * {@inheritDoc}
424      */
425     @Override
426     public boolean handles(final Event event) {
427         if (Event.TYPE_BLUR.equals(event.getType()) || Event.TYPE_FOCUS.equals(event.getType())) {
428             return true;
429         }
430         return super.handles(event);
431     }
432 
433     /**
434      * Returns the value of the attribute {@code ping}.
435      *
436      * @return the value of the attribute {@code ping}
437      */
438     public final String getPingAttribute() {
439         return getAttributeDirect("ping");
440     }
441 
442     /**
443      * Returns the value of the attribute {@code download}.
444      *
445      * @return the value of the attribute {@code download}
446      */
447     public final String getDownloadAttribute() {
448         return getAttributeDirect("download");
449     }
450 }