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