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><link rel="stylesheet" type="text/css" href="..." /></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><link rel="stylesheet" type="text/css" href="..." /></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 }