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 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><link rel="stylesheet" type="text/css" href="..." /></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 (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 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 WebClient webClient, final String type) {
280 if (!webClient.isJavaScriptEngineEnabled()) {
281 return;
282 }
283
284 final HTMLLinkElement link = getScriptableObject();
285 final Event event = new Event(this, type);
286 link.executeEventLocally(event);
287 }
288
289 /**
290 * {@inheritDoc}
291 */
292 @Override
293 public void onAllChildrenAddedToPage(final boolean postponed) {
294 if (getOwnerDocument() instanceof XmlPage) {
295 return;
296 }
297 if (LOG.isDebugEnabled()) {
298 LOG.debug("Link node added: " + asXml());
299 }
300
301 if (isStyleSheetLink()) {
302 final WebClient webClient = getPage().getWebClient();
303 if (!webClient.getOptions().isCssEnabled()) {
304 if (LOG.isDebugEnabled()) {
305 LOG.debug("Stylesheet Link found but ignored because css support is disabled ("
306 + asXml().replaceAll("[\\r\\n]", "") + ").");
307 }
308 return;
309 }
310
311 if (!webClient.isJavaScriptEngineEnabled()) {
312 if (LOG.isDebugEnabled()) {
313 LOG.debug("Stylesheet Link found but ignored because javascript engine is disabled ("
314 + asXml().replaceAll("[\\r\\n]", "") + ").");
315 }
316 return;
317 }
318
319 final PostponedAction action = new PostponedAction(getPage(), "Loading of link " + this) {
320 @Override
321 public void execute() {
322 final HTMLLinkElement linkElem = HtmlLink.this.getScriptableObject();
323 // force loading, caching inside the link
324 linkElem.getSheet();
325 }
326 };
327
328 final AbstractJavaScriptEngine<?> engine = webClient.getJavaScriptEngine();
329 if (postponed) {
330 engine.addPostponedAction(action);
331 }
332 else {
333 try {
334 action.execute();
335 }
336 catch (final RuntimeException e) {
337 throw e;
338 }
339 catch (final Exception e) {
340 throw new RuntimeException(e);
341 }
342 }
343
344 return;
345 }
346
347 if (LOG.isDebugEnabled()) {
348 LOG.debug("Link type '" + getRelAttribute() + "' not supported ("
349 + asXml().replaceAll("[\\r\\n]", "") + ").");
350 }
351 }
352
353 /**
354 * Returns the associated style sheet (only valid for links of type
355 * <code><link rel="stylesheet" type="text/css" href="..." /></code>).
356 * @return the associated style sheet
357 */
358 public CssStyleSheet getSheet() {
359 if (sheet_ == null) {
360 sheet_ = CssStyleSheet.loadStylesheet(this, this, null);
361 }
362 return sheet_;
363 }
364
365 /**
366 * @return true if the rel attribute is 'stylesheet'
367 */
368 public boolean isStyleSheetLink() {
369 final String rel = getRelAttribute();
370 if (rel != null) {
371 return ArrayUtils.containsIgnoreCase(StringUtils.splitAtBlank(rel), "stylesheet");
372 }
373 return false;
374 }
375
376 /**
377 * @return true if the rel attribute is 'modulepreload'
378 */
379 public boolean isModulePreloadLink() {
380 final String rel = getRelAttribute();
381 if (rel != null) {
382 return ArrayUtils.containsIgnoreCase(StringUtils.splitAtBlank(rel), "modulepreload");
383 }
384 return false;
385 }
386
387 /**
388 * <p><span style="color:red">Experimental API: May be changed in next release
389 * and may not yet work perfectly!</span></p>
390 *
391 * Verifies if the provided node is a link node pointing to an active stylesheet.
392 *
393 * @return true if the provided node is a stylesheet link
394 */
395 public boolean isActiveStyleSheetLink() {
396 if (isStyleSheetLink()) {
397 final String media = getMediaAttribute();
398 if (StringUtils.isBlank(media)) {
399 return true;
400 }
401
402 final MediaListImpl mediaList =
403 CssStyleSheet.parseMedia(media, getPage().getWebClient());
404 return CssStyleSheet.isActive(mediaList, getPage().getEnclosingWindow());
405 }
406 return false;
407 }
408 }