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.html;
16  
17  import static org.htmlunit.BrowserVersionFeatures.HTMLLINK_CHECK_RESPONSE_TYPE_FOR_STYLESHEET;
18  
19  import java.io.IOException;
20  import java.net.MalformedURLException;
21  import java.net.URL;
22  import java.util.Map;
23  
24  import org.apache.commons.logging.Log;
25  import org.apache.commons.logging.LogFactory;
26  import org.htmlunit.BrowserVersion;
27  import org.htmlunit.SgmlPage;
28  import org.htmlunit.WebClient;
29  import org.htmlunit.WebRequest;
30  import org.htmlunit.WebResponse;
31  import org.htmlunit.css.CssStyleSheet;
32  import org.htmlunit.cssparser.dom.MediaListImpl;
33  import org.htmlunit.javascript.PostponedAction;
34  import org.htmlunit.javascript.host.event.Event;
35  import org.htmlunit.javascript.host.html.HTMLLinkElement;
36  import org.htmlunit.util.ArrayUtils;
37  import org.htmlunit.util.MimeType;
38  import org.htmlunit.util.StringUtils;
39  import org.htmlunit.xml.XmlPage;
40  
41  /**
42   * Wrapper for the HTML element "link". <b>Note:</b> This is not a clickable link,
43   * that one is an HtmlAnchor
44   *
45   * @author Mike Bowler
46   * @author David K. Taylor
47   * @author Christian Sell
48   * @author Ahmed Ashour
49   * @author Marc Guillemot
50   * @author Frank Danek
51   * @author Ronald Brill
52   */
53  public class HtmlLink extends HtmlElement {
54      private static final Log LOG = LogFactory.getLog(HtmlLink.class);
55  
56      /** The HTML tag represented by this element. */
57      public static final String TAG_NAME = "link";
58  
59      /**
60       * The associated style sheet (only valid for links of type
61       * <code>&lt;link rel="stylesheet" type="text/css" href="..." /&gt;</code>).
62       */
63      private CssStyleSheet sheet_;
64  
65      /**
66       * Creates an instance of HtmlLink
67       *
68       * @param qualifiedName the qualified name of the element type to instantiate
69       * @param page the HtmlPage that contains this element
70       * @param attributes the initial attributes
71       */
72      HtmlLink(final String qualifiedName, final SgmlPage page,
73              final Map<String, DomAttr> attributes) {
74          super(qualifiedName, page, attributes);
75      }
76  
77      /**
78       * Returns the value of the attribute {@code charset}. Refer to the
79       * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
80       * documentation for details on the use of this attribute.
81       *
82       * @return the value of the attribute {@code charset}
83       *         or an empty string if that attribute isn't defined.
84       */
85      public final String getCharsetAttribute() {
86          return getAttributeDirect("charset");
87      }
88  
89      /**
90       * Returns the value of the attribute {@code href}. Refer to the
91       * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
92       * documentation for details on the use of this attribute.
93       *
94       * @return the value of the attribute {@code href}
95       *         or an empty string if that attribute isn't defined.
96       */
97      public final String getHrefAttribute() {
98          return getAttributeDirect("href");
99      }
100 
101     /**
102      * Returns the value of the attribute {@code hreflang}. Refer to the
103      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
104      * documentation for details on the use of this attribute.
105      *
106      * @return the value of the attribute {@code hreflang}
107      *         or an empty string if that attribute isn't defined.
108      */
109     public final String getHrefLangAttribute() {
110         return getAttributeDirect("hreflang");
111     }
112 
113     /**
114      * Returns the value of the attribute {@code type}. Refer to the
115      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
116      * documentation for details on the use of this attribute.
117      *
118      * @return the value of the attribute {@code type}
119      *         or an empty string if that attribute isn't defined.
120      */
121     public final String getTypeAttribute() {
122         return getAttributeDirect(TYPE_ATTRIBUTE);
123     }
124 
125     /**
126      * Returns the value of the attribute {@code rel}. Refer to the
127      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
128      * documentation for details on the use of this attribute.
129      *
130      * @return the value of the attribute {@code rel}
131      *         or an empty string if that attribute isn't defined.
132      */
133     public final String getRelAttribute() {
134         return getAttributeDirect("rel");
135     }
136 
137     /**
138      * Returns the value of the attribute {@code rev}. Refer to the
139      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
140      * documentation for details on the use of this attribute.
141      *
142      * @return the value of the attribute {@code rev}
143      *         or an empty string if that attribute isn't defined.
144      */
145     public final String getRevAttribute() {
146         return getAttributeDirect("rev");
147     }
148 
149     /**
150      * Returns the value of the attribute {@code media}. Refer to the
151      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
152      * documentation for details on the use of this attribute.
153      *
154      * @return the value of the attribute {@code media}
155      *         or an empty string if that attribute isn't defined.
156      */
157     public final String getMediaAttribute() {
158         return getAttributeDirect("media");
159     }
160 
161     /**
162      * Returns the value of the attribute {@code target}. Refer to the
163      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
164      * documentation for details on the use of this attribute.
165      *
166      * @return the value of the attribute {@code target}
167      *         or an empty string if that attribute isn't defined.
168      */
169     public final String getTargetAttribute() {
170         return getAttributeDirect("target");
171     }
172 
173     /**
174      * <span style="color:red">POTENIAL PERFORMANCE KILLER - DOWNLOADS THE RESOURCE - USE AT YOUR OWN RISK.</span><br>
175      * If the linked content is not already downloaded it triggers a download. Then it stores the response
176      * for later use.<br>
177      *
178      * @param downloadIfNeeded indicates if a request should be performed this hasn't been done previously
179      * @return {@code null} if no download should be performed and when this wasn't already done; the response
180      *         received when performing a request for the content referenced by this tag otherwise
181      * @throws IOException if an error occurs while downloading the content
182      */
183     public WebResponse getWebResponse(final boolean downloadIfNeeded) throws IOException {
184         return getWebResponse(downloadIfNeeded, null, false, null);
185     }
186 
187     /**
188      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
189      *
190      * If the linked content is not already downloaded it triggers a download. Then it stores the response
191      * for later use.<br>
192      *
193      * @param downloadIfNeeded indicates if a request should be performed this hasn't been done previously
194      * @param request the request; if null getWebRequest() is called to create one
195      * @param isStylesheetRequest true if this should return a stylesheet
196      * @param type the type definined for the stylesheet link
197      * @return {@code null} if no download should be performed and when this wasn't already done; the response
198      *         received when performing a request for the content referenced by this tag otherwise
199      * @throws IOException if an error occurs while downloading the content
200      */
201     public WebResponse getWebResponse(final boolean downloadIfNeeded, WebRequest request,
202             final boolean isStylesheetRequest, final String type) throws IOException {
203         final WebClient webClient = getPage().getWebClient();
204         if (null == request) {
205             request = getWebRequest();
206         }
207 
208         if (downloadIfNeeded) {
209             try {
210                 final WebResponse response = webClient.loadWebResponse(request);
211                 if (response.isSuccess()) {
212                     if (isStylesheetRequest
213                             && webClient.getBrowserVersion()
214                                  .hasFeature(HTMLLINK_CHECK_RESPONSE_TYPE_FOR_STYLESHEET)) {
215 
216                         if (StringUtils.isNotBlank(type)
217                                 && !MimeType.TEXT_CSS.equals(type)) {
218                             return null;
219                         }
220 
221                         final String respType = response.getContentType();
222                         if (StringUtils.isNotBlank(respType)
223                                 && !MimeType.TEXT_CSS.equals(respType)) {
224                             executeEvent(webClient, Event.TYPE_ERROR);
225                             return response;
226                         }
227                     }
228                     executeEvent(webClient, Event.TYPE_LOAD);
229                     return response;
230                 }
231                 executeEvent(webClient, Event.TYPE_ERROR);
232                 return response;
233             }
234             catch (final IOException e) {
235                 executeEvent(webClient, Event.TYPE_ERROR);
236                 throw e;
237             }
238         }
239 
240         // retrieve the response, from the cache if available
241         return webClient.getCache().getCachedResponse(request);
242     }
243 
244     /**
245      * Returns the request which will allow us to retrieve the content referenced by the {@code href} attribute.
246      * @return the request which will allow us to retrieve the content referenced by the {@code href} attribute
247      * @throws MalformedURLException in case of problem resolving the URL
248      */
249     public WebRequest getWebRequest() throws MalformedURLException {
250         final HtmlPage page = (HtmlPage) getPage();
251         final URL url = page.getFullyQualifiedUrl(getHrefAttribute());
252 
253         final BrowserVersion browser = page.getWebClient().getBrowserVersion();
254         final WebRequest request = new WebRequest(url, browser.getCssAcceptHeader(), browser.getAcceptEncodingHeader());
255         // use the page encoding even if this is a GET requests
256         request.setCharset(page.getCharset());
257         request.setRefererHeader(page.getUrl());
258 
259         return request;
260     }
261 
262     /**
263      * {@inheritDoc}
264      */
265     @Override
266     public DisplayStyle getDefaultStyleDisplay() {
267         return DisplayStyle.NONE;
268     }
269 
270     /**
271      * {@inheritDoc}
272      */
273     @Override
274     public boolean mayBeDisplayed() {
275         return false;
276     }
277 
278     private void executeEvent(final WebClient webClient, final String type) {
279         if (!webClient.isJavaScriptEngineEnabled()) {
280             return;
281         }
282 
283         final HTMLLinkElement link = getScriptableObject();
284         final Event event = new Event(this, type);
285         link.executeEventLocally(event);
286     }
287 
288     /**
289      * {@inheritDoc}
290      */
291     @Override
292     public void onAllChildrenAddedToPage(final boolean postponed) {
293         if (getOwnerDocument() instanceof XmlPage) {
294             return;
295         }
296         if (LOG.isDebugEnabled()) {
297             LOG.debug("Link node added: " + asXml());
298         }
299 
300         if (isStyleSheetLink()) {
301             final WebClient webClient = getPage().getWebClient();
302             if (!webClient.getOptions().isCssEnabled()) {
303                 if (LOG.isDebugEnabled()) {
304                     LOG.debug("Stylesheet Link found but ignored because css support is disabled ("
305                                 + asXml().replaceAll("[\\r\\n]", "") + ").");
306                 }
307                 return;
308             }
309 
310             if (!webClient.isJavaScriptEngineEnabled()) {
311                 if (LOG.isDebugEnabled()) {
312                     LOG.debug("Stylesheet Link found but ignored because javascript engine is disabled ("
313                                 + asXml().replaceAll("[\\r\\n]", "") + ").");
314                 }
315                 return;
316             }
317 
318             final PostponedAction action = new PostponedAction(getPage(), "Loading of link " + this) {
319                 @Override
320                 public void execute() {
321                     final HTMLLinkElement linkElem = HtmlLink.this.getScriptableObject();
322                     // force loading, caching inside the link
323                     linkElem.getSheet();
324                 }
325             };
326 
327             if (postponed) {
328                 webClient.getJavaScriptEngine().addPostponedAction(action);
329             }
330             else {
331                 try {
332                     action.execute();
333                 }
334                 catch (final RuntimeException e) {
335                     throw e;
336                 }
337                 catch (final Exception e) {
338                     throw new RuntimeException(e);
339                 }
340             }
341 
342             return;
343         }
344 
345         if (LOG.isDebugEnabled()) {
346             LOG.debug("Link type '" + getRelAttribute() + "' not supported ("
347                         + asXml().replaceAll("[\\r\\n]", "") + ").");
348         }
349     }
350 
351     /**
352      * Returns the associated style sheet (only valid for links of type
353      * <code>&lt;link rel="stylesheet" type="text/css" href="..." /&gt;</code>).
354      * @return the associated style sheet
355      */
356     public CssStyleSheet getSheet() {
357         if (sheet_ == null) {
358             sheet_ = CssStyleSheet.loadStylesheet(this, this, null);
359         }
360         return sheet_;
361     }
362 
363     /**
364      * @return true if the rel attribute is 'stylesheet'
365      */
366     public boolean isStyleSheetLink() {
367         final String rel = getRelAttribute();
368         if (rel != null) {
369             return ArrayUtils.containsIgnoreCase(StringUtils.splitAtBlank(rel), "stylesheet");
370         }
371         return false;
372     }
373 
374     /**
375      * @return true if the rel attribute is 'modulepreload'
376      */
377     public boolean isModulePreloadLink() {
378         final String rel = getRelAttribute();
379         if (rel != null) {
380             return ArrayUtils.containsIgnoreCase(StringUtils.splitAtBlank(rel), "modulepreload");
381         }
382         return false;
383     }
384 
385     /**
386      * <p><span style="color:red">Experimental API: May be changed in next release
387      * and may not yet work perfectly!</span></p>
388      *
389      * Verifies if the provided node is a link node pointing to an active stylesheet.
390      *
391      * @return true if the provided node is a stylesheet link
392      */
393     public boolean isActiveStyleSheetLink() {
394         if (isStyleSheetLink()) {
395             final String media = getMediaAttribute();
396             if (StringUtils.isBlank(media)) {
397                 return true;
398             }
399 
400             final MediaListImpl mediaList =
401                     CssStyleSheet.parseMedia(media, getPage().getWebClient());
402             return CssStyleSheet.isActive(mediaList, getPage().getEnclosingWindow());
403         }
404         return false;
405     }
406 }