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.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.AbstractJavaScriptEngine;
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 <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
47   * @author David K. Taylor
48   * @author <a href="mailto:cse@dynabean.de">Christian Sell</a>
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 (null == request) {
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 (org.apache.commons.lang3.StringUtils.isNotBlank(type)
218                                 && !MimeType.TEXT_CSS.equals(type)) {
219                             return null;
220                         }
221 
222                         final String respType = response.getContentType();
223                         if (org.apache.commons.lang3.StringUtils.isNotBlank(respType)
224                                 && !MimeType.TEXT_CSS.equals(respType)) {
225                             executeEvent(Event.TYPE_ERROR);
226                             return response;
227                         }
228                     }
229                     executeEvent(Event.TYPE_LOAD);
230                     return response;
231                 }
232                 executeEvent(Event.TYPE_ERROR);
233                 return response;
234             }
235             catch (final IOException e) {
236                 executeEvent(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         return request;
261     }
262 
263     /**
264      * {@inheritDoc}
265      */
266     @Override
267     public DisplayStyle getDefaultStyleDisplay() {
268         return DisplayStyle.NONE;
269     }
270 
271     /**
272      * {@inheritDoc}
273      */
274     @Override
275     public boolean mayBeDisplayed() {
276         return false;
277     }
278 
279     private void executeEvent(final String type) {
280         final HTMLLinkElement link = getScriptableObject();
281         final Event event = new Event(this, type);
282         link.executeEventLocally(event);
283     }
284 
285     /**
286      * {@inheritDoc}
287      */
288     @Override
289     public void onAllChildrenAddedToPage(final boolean postponed) {
290         if (getOwnerDocument() instanceof XmlPage) {
291             return;
292         }
293         if (LOG.isDebugEnabled()) {
294             LOG.debug("Link node added: " + asXml());
295         }
296 
297         final boolean isStyleSheetLink = isStyleSheetLink();
298 
299         if (isStyleSheetLink) {
300             final WebClient webClient = getPage().getWebClient();
301             if (!webClient.getOptions().isCssEnabled()) {
302                 if (LOG.isDebugEnabled()) {
303                     LOG.debug("Stylesheet Link found but ignored because css support is disabled ("
304                                 + asXml().replaceAll("[\\r\\n]", "") + ").");
305                 }
306                 return;
307             }
308 
309             if (!webClient.isJavaScriptEngineEnabled()) {
310                 if (LOG.isDebugEnabled()) {
311                     LOG.debug("Stylesheet Link found but ignored because javascript engine is disabled ("
312                                 + asXml().replaceAll("[\\r\\n]", "") + ").");
313                 }
314                 return;
315             }
316 
317             final PostponedAction action = new PostponedAction(getPage(), "Loading of link " + this) {
318                 @Override
319                 public void execute() {
320                     final HTMLLinkElement linkElem = HtmlLink.this.getScriptableObject();
321                     // force loading, caching inside the link
322                     linkElem.getSheet();
323                 }
324             };
325 
326             final AbstractJavaScriptEngine<?> engine = webClient.getJavaScriptEngine();
327             if (postponed) {
328                 engine.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 (org.apache.commons.lang3.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 }