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