1 /*
2 * Copyright (C) 1999 Lars Knoll (knoll@kde.org)
3 * (C) 1999 Antti Koivisto (koivisto@kde.org)
4 * (C) 2001 Dirk Mueller (mueller@kde.org)
5 * Copyright (C) 2003, 2006, 2007, 2008, 2009, 2010 Apple Inc. All rights reserved.
6 * Copyright (C) 2009 Rob Buis (rwlbuis@gmail.com)
7 *
8 * This library is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU Library General Public
10 * License as published by the Free Software Foundation; either
11 * version 2 of the License, or (at your option) any later version.
12 *
13 * This library is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * Library General Public License for more details.
17 *
18 * You should have received a copy of the GNU Library General Public License
19 * along with this library; see the file COPYING.LIB. If not, write to
20 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 * Boston, MA 02110-1301, USA.
22 */
23
24 #include "config.h"
25 #include "HTMLLinkElement.h"
26
27 #include "Attribute.h"
28 #include "CachedCSSStyleSheet.h"
29 #include "CachedResource.h"
30 #include "CachedResourceLoader.h"
31 #include "CSSStyleSelector.h"
32 #include "Document.h"
33 #include "Frame.h"
34 #include "FrameLoader.h"
35 #include "FrameLoaderClient.h"
36 #include "FrameTree.h"
37 #include "FrameView.h"
38 #include "HTMLNames.h"
39 #include "HTMLParserIdioms.h"
40 #include "MediaList.h"
41 #include "MediaQueryEvaluator.h"
42 #include "Page.h"
43 #include "ResourceHandle.h"
44 #include "ScriptEventListener.h"
45 #include "Settings.h"
46 #include <wtf/StdLibExtras.h>
47
48 namespace WebCore {
49
50 using namespace HTMLNames;
51
HTMLLinkElement(const QualifiedName & tagName,Document * document,bool createdByParser)52 inline HTMLLinkElement::HTMLLinkElement(const QualifiedName& tagName, Document* document, bool createdByParser)
53 : HTMLElement(tagName, document)
54 #if ENABLE(LINK_PREFETCH)
55 , m_onloadTimer(this, &HTMLLinkElement::onloadTimerFired)
56 #endif
57 , m_disabledState(Unset)
58 , m_loading(false)
59 , m_createdByParser(createdByParser)
60 , m_isInShadowTree(false)
61 , m_pendingSheetType(None)
62 {
63 ASSERT(hasTagName(linkTag));
64 }
65
create(const QualifiedName & tagName,Document * document,bool createdByParser)66 PassRefPtr<HTMLLinkElement> HTMLLinkElement::create(const QualifiedName& tagName, Document* document, bool createdByParser)
67 {
68 return adoptRef(new HTMLLinkElement(tagName, document, createdByParser));
69 }
70
~HTMLLinkElement()71 HTMLLinkElement::~HTMLLinkElement()
72 {
73 if (m_sheet)
74 m_sheet->clearOwnerNode();
75
76 if (m_cachedSheet) {
77 m_cachedSheet->removeClient(this);
78 removePendingSheet();
79 }
80
81 #if ENABLE(LINK_PREFETCH)
82 if (m_cachedLinkResource)
83 m_cachedLinkResource->removeClient(this);
84 #endif
85 }
86
setDisabledState(bool _disabled)87 void HTMLLinkElement::setDisabledState(bool _disabled)
88 {
89 DisabledState oldDisabledState = m_disabledState;
90 m_disabledState = _disabled ? Disabled : EnabledViaScript;
91 if (oldDisabledState != m_disabledState) {
92 // If we change the disabled state while the sheet is still loading, then we have to
93 // perform three checks:
94 if (isLoading()) {
95 // Check #1: The sheet becomes disabled while loading.
96 if (m_disabledState == Disabled)
97 removePendingSheet();
98
99 // Check #2: An alternate sheet becomes enabled while it is still loading.
100 if (m_relAttribute.m_isAlternate && m_disabledState == EnabledViaScript)
101 addPendingSheet(Blocking);
102
103 // Check #3: A main sheet becomes enabled while it was still loading and
104 // after it was disabled via script. It takes really terrible code to make this
105 // happen (a double toggle for no reason essentially). This happens on
106 // virtualplastic.net, which manages to do about 12 enable/disables on only 3
107 // sheets. :)
108 if (!m_relAttribute.m_isAlternate && m_disabledState == EnabledViaScript && oldDisabledState == Disabled)
109 addPendingSheet(Blocking);
110
111 // If the sheet is already loading just bail.
112 return;
113 }
114
115 // Load the sheet, since it's never been loaded before.
116 if (!m_sheet && m_disabledState == EnabledViaScript)
117 process();
118 else
119 document()->styleSelectorChanged(DeferRecalcStyle); // Update the style selector.
120 }
121 }
122
sheet() const123 StyleSheet* HTMLLinkElement::sheet() const
124 {
125 return m_sheet.get();
126 }
127
parseMappedAttribute(Attribute * attr)128 void HTMLLinkElement::parseMappedAttribute(Attribute* attr)
129 {
130 if (attr->name() == relAttr) {
131 tokenizeRelAttribute(attr->value(), m_relAttribute);
132 process();
133 } else if (attr->name() == hrefAttr) {
134 m_url = document()->completeURL(stripLeadingAndTrailingHTMLSpaces(attr->value()));
135 process();
136 } else if (attr->name() == typeAttr) {
137 m_type = attr->value();
138 process();
139 } else if (attr->name() == mediaAttr) {
140 m_media = attr->value().string().lower();
141 process();
142 } else if (attr->name() == disabledAttr)
143 setDisabledState(!attr->isNull());
144 else if (attr->name() == onbeforeloadAttr)
145 setAttributeEventListener(eventNames().beforeloadEvent, createAttributeEventListener(this, attr));
146 #if ENABLE(LINK_PREFETCH)
147 else if (attr->name() == onloadAttr)
148 setAttributeEventListener(eventNames().loadEvent, createAttributeEventListener(this, attr));
149 else if (attr->name() == onerrorAttr)
150 setAttributeEventListener(eventNames().errorEvent, createAttributeEventListener(this, attr));
151 #endif
152 else {
153 if (attr->name() == titleAttr && m_sheet)
154 m_sheet->setTitle(attr->value());
155 HTMLElement::parseMappedAttribute(attr);
156 }
157 }
158
tokenizeRelAttribute(const AtomicString & rel,RelAttribute & relAttribute)159 void HTMLLinkElement::tokenizeRelAttribute(const AtomicString& rel, RelAttribute& relAttribute)
160 {
161 relAttribute.m_isStyleSheet = false;
162 relAttribute.m_isIcon = false;
163 relAttribute.m_isAlternate = false;
164 relAttribute.m_isDNSPrefetch = false;
165 #if ENABLE(LINK_PREFETCH)
166 relAttribute.m_isLinkPrefetch = false;
167 relAttribute.m_isLinkSubresource = false;
168 #endif
169 #ifdef ANDROID_APPLE_TOUCH_ICON
170 relAttribute.m_isTouchIcon = false;
171 relAttribute.m_isPrecomposedTouchIcon = false;
172 #endif
173 if (equalIgnoringCase(rel, "stylesheet"))
174 relAttribute.m_isStyleSheet = true;
175 else if (equalIgnoringCase(rel, "icon") || equalIgnoringCase(rel, "shortcut icon"))
176 relAttribute.m_isIcon = true;
177 #ifdef ANDROID_APPLE_TOUCH_ICON
178 else if (equalIgnoringCase(rel, "apple-touch-icon"))
179 relAttribute.m_isTouchIcon = true;
180 else if (equalIgnoringCase(rel, "apple-touch-icon-precomposed"))
181 relAttribute.m_isPrecomposedTouchIcon = true;
182 #endif
183 else if (equalIgnoringCase(rel, "dns-prefetch"))
184 relAttribute.m_isDNSPrefetch = true;
185 else if (equalIgnoringCase(rel, "alternate stylesheet") || equalIgnoringCase(rel, "stylesheet alternate")) {
186 relAttribute.m_isStyleSheet = true;
187 relAttribute.m_isAlternate = true;
188 } else {
189 // Tokenize the rel attribute and set bits based on specific keywords that we find.
190 String relString = rel.string();
191 relString.replace('\n', ' ');
192 Vector<String> list;
193 relString.split(' ', list);
194 Vector<String>::const_iterator end = list.end();
195 for (Vector<String>::const_iterator it = list.begin(); it != end; ++it) {
196 if (equalIgnoringCase(*it, "stylesheet"))
197 relAttribute.m_isStyleSheet = true;
198 else if (equalIgnoringCase(*it, "alternate"))
199 relAttribute.m_isAlternate = true;
200 else if (equalIgnoringCase(*it, "icon"))
201 relAttribute.m_isIcon = true;
202 #if ENABLE(LINK_PREFETCH)
203 else if (equalIgnoringCase(*it, "prefetch"))
204 relAttribute.m_isLinkPrefetch = true;
205 else if (equalIgnoringCase(*it, "subresource"))
206 relAttribute.m_isLinkSubresource = true;
207 #endif
208 }
209 }
210 }
211
checkBeforeLoadEvent()212 bool HTMLLinkElement::checkBeforeLoadEvent()
213 {
214 RefPtr<Document> originalDocument = document();
215 if (!dispatchBeforeLoadEvent(m_url))
216 return false;
217 // A beforeload handler might have removed us from the document or changed the document.
218 if (!inDocument() || document() != originalDocument)
219 return false;
220 return true;
221 }
222
process()223 void HTMLLinkElement::process()
224 {
225 if (!inDocument() || m_isInShadowTree) {
226 ASSERT(!m_sheet);
227 return;
228 }
229
230 String type = m_type.lower();
231
232 // IE extension: location of small icon for locationbar / bookmarks
233 // We'll record this URL per document, even if we later only use it in top level frames
234 if (m_relAttribute.m_isIcon && m_url.isValid() && !m_url.isEmpty()) {
235 if (!checkBeforeLoadEvent())
236 return;
237 document()->setIconURL(m_url.string(), type);
238 }
239
240 #ifdef ANDROID_APPLE_TOUCH_ICON
241 if ((m_relAttribute.m_isTouchIcon || m_relAttribute.m_isPrecomposedTouchIcon) && m_url.isValid()
242 && !m_url.isEmpty() && document()->frame())
243 document()->frame()->loader()->client()
244 ->dispatchDidReceiveTouchIconURL(m_url.string(),
245 m_relAttribute.m_isPrecomposedTouchIcon);
246 #endif
247
248 if (m_relAttribute.m_isDNSPrefetch) {
249 Settings* settings = document()->settings();
250 // FIXME: The href attribute of the link element can be in "//hostname" form, and we shouldn't attempt
251 // to complete that as URL <https://bugs.webkit.org/show_bug.cgi?id=48857>.
252 if (settings && settings->dnsPrefetchingEnabled() && m_url.isValid() && !m_url.isEmpty())
253 ResourceHandle::prepareForURL(m_url);
254 }
255
256 #if ENABLE(LINK_PREFETCH)
257 if ((m_relAttribute.m_isLinkPrefetch || m_relAttribute.m_isLinkSubresource) && m_url.isValid() && document()->frame()) {
258 if (!checkBeforeLoadEvent())
259 return;
260 ResourceLoadPriority priority = ResourceLoadPriorityUnresolved;
261 if (m_relAttribute.m_isLinkSubresource)
262 priority = ResourceLoadPriorityLow;
263
264 m_cachedLinkResource = document()->cachedResourceLoader()->requestLinkResource(m_url, priority);
265 if (m_cachedLinkResource)
266 m_cachedLinkResource->addClient(this);
267 }
268 #endif
269
270 bool acceptIfTypeContainsTextCSS = document()->page() && document()->page()->settings() && document()->page()->settings()->treatsAnyTextCSSLinkAsStylesheet();
271
272 if (m_disabledState != Disabled && (m_relAttribute.m_isStyleSheet || (acceptIfTypeContainsTextCSS && type.contains("text/css")))
273 && document()->frame() && m_url.isValid()) {
274
275 String charset = getAttribute(charsetAttr);
276 if (charset.isEmpty() && document()->frame())
277 charset = document()->charset();
278
279 if (m_cachedSheet) {
280 removePendingSheet();
281 m_cachedSheet->removeClient(this);
282 m_cachedSheet = 0;
283 }
284
285 if (!checkBeforeLoadEvent())
286 return;
287
288 m_loading = true;
289
290 bool mediaQueryMatches = true;
291 if (!m_media.isEmpty()) {
292 RefPtr<RenderStyle> documentStyle = CSSStyleSelector::styleForDocument(document());
293 RefPtr<MediaList> media = MediaList::createAllowingDescriptionSyntax(m_media);
294 MediaQueryEvaluator evaluator(document()->frame()->view()->mediaType(), document()->frame(), documentStyle.get());
295 mediaQueryMatches = evaluator.eval(media.get());
296 }
297
298 // Don't hold up render tree construction and script execution on stylesheets
299 // that are not needed for the rendering at the moment.
300 bool blocking = mediaQueryMatches && !isAlternate();
301 addPendingSheet(blocking ? Blocking : NonBlocking);
302
303 // Load stylesheets that are not needed for the rendering immediately with low priority.
304 ResourceLoadPriority priority = blocking ? ResourceLoadPriorityUnresolved : ResourceLoadPriorityVeryLow;
305 m_cachedSheet = document()->cachedResourceLoader()->requestCSSStyleSheet(m_url, charset, priority);
306
307 if (m_cachedSheet)
308 m_cachedSheet->addClient(this);
309 else {
310 // The request may have been denied if (for example) the stylesheet is local and the document is remote.
311 m_loading = false;
312 removePendingSheet();
313 }
314 } else if (m_sheet) {
315 // we no longer contain a stylesheet, e.g. perhaps rel or type was changed
316 m_sheet = 0;
317 document()->styleSelectorChanged(DeferRecalcStyle);
318 }
319 }
320
insertedIntoDocument()321 void HTMLLinkElement::insertedIntoDocument()
322 {
323 HTMLElement::insertedIntoDocument();
324
325 m_isInShadowTree = isInShadowTree();
326 if (m_isInShadowTree)
327 return;
328
329 document()->addStyleSheetCandidateNode(this, m_createdByParser);
330
331 process();
332 }
333
removedFromDocument()334 void HTMLLinkElement::removedFromDocument()
335 {
336 HTMLElement::removedFromDocument();
337
338 if (m_isInShadowTree) {
339 ASSERT(!m_sheet);
340 return;
341 }
342 document()->removeStyleSheetCandidateNode(this);
343
344 if (m_sheet) {
345 ASSERT(m_sheet->ownerNode() == this);
346 m_sheet->clearOwnerNode();
347 m_sheet = 0;
348 }
349
350 if (document()->renderer())
351 document()->styleSelectorChanged(DeferRecalcStyle);
352 }
353
finishParsingChildren()354 void HTMLLinkElement::finishParsingChildren()
355 {
356 m_createdByParser = false;
357 HTMLElement::finishParsingChildren();
358 }
359
setCSSStyleSheet(const String & href,const KURL & baseURL,const String & charset,const CachedCSSStyleSheet * sheet)360 void HTMLLinkElement::setCSSStyleSheet(const String& href, const KURL& baseURL, const String& charset, const CachedCSSStyleSheet* sheet)
361 {
362 if (!inDocument()) {
363 ASSERT(!m_sheet);
364 return;
365 }
366
367 m_sheet = CSSStyleSheet::create(this, href, baseURL, charset);
368
369 bool strictParsing = !document()->inQuirksMode();
370 bool enforceMIMEType = strictParsing;
371 bool crossOriginCSS = false;
372 bool validMIMEType = false;
373 bool needsSiteSpecificQuirks = document()->page() && document()->page()->settings()->needsSiteSpecificQuirks();
374
375 // Check to see if we should enforce the MIME type of the CSS resource in strict mode.
376 // Running in iWeb 2 is one example of where we don't want to - <rdar://problem/6099748>
377 if (enforceMIMEType && document()->page() && !document()->page()->settings()->enforceCSSMIMETypeInNoQuirksMode())
378 enforceMIMEType = false;
379
380 #if defined(BUILDING_ON_TIGER) || defined(BUILDING_ON_LEOPARD)
381 if (enforceMIMEType && needsSiteSpecificQuirks) {
382 // Covers both http and https, with or without "www."
383 if (baseURL.string().contains("mcafee.com/japan/", false))
384 enforceMIMEType = false;
385 }
386 #endif
387
388 String sheetText = sheet->sheetText(enforceMIMEType, &validMIMEType);
389 m_sheet->parseString(sheetText, strictParsing);
390
391 // If we're loading a stylesheet cross-origin, and the MIME type is not
392 // standard, require the CSS to at least start with a syntactically
393 // valid CSS rule.
394 // This prevents an attacker playing games by injecting CSS strings into
395 // HTML, XML, JSON, etc. etc.
396 if (!document()->securityOrigin()->canRequest(baseURL))
397 crossOriginCSS = true;
398
399 if (crossOriginCSS && !validMIMEType && !m_sheet->hasSyntacticallyValidCSSHeader())
400 m_sheet = CSSStyleSheet::create(this, href, baseURL, charset);
401
402 if (strictParsing && needsSiteSpecificQuirks) {
403 // Work around <https://bugs.webkit.org/show_bug.cgi?id=28350>.
404 DEFINE_STATIC_LOCAL(const String, slashKHTMLFixesDotCss, ("/KHTMLFixes.css"));
405 DEFINE_STATIC_LOCAL(const String, mediaWikiKHTMLFixesStyleSheet, ("/* KHTML fix stylesheet */\n/* work around the horizontal scrollbars */\n#column-content { margin-left: 0; }\n\n"));
406 // There are two variants of KHTMLFixes.css. One is equal to mediaWikiKHTMLFixesStyleSheet,
407 // while the other lacks the second trailing newline.
408 if (baseURL.string().endsWith(slashKHTMLFixesDotCss) && !sheetText.isNull() && mediaWikiKHTMLFixesStyleSheet.startsWith(sheetText)
409 && sheetText.length() >= mediaWikiKHTMLFixesStyleSheet.length() - 1) {
410 ASSERT(m_sheet->length() == 1);
411 ExceptionCode ec;
412 m_sheet->deleteRule(0, ec);
413 }
414 }
415
416 m_sheet->setTitle(title());
417
418 RefPtr<MediaList> media = MediaList::createAllowingDescriptionSyntax(m_media);
419 m_sheet->setMedia(media.get());
420
421 m_loading = false;
422 m_sheet->checkLoaded();
423 }
424
isLoading() const425 bool HTMLLinkElement::isLoading() const
426 {
427 if (m_loading)
428 return true;
429 if (!m_sheet)
430 return false;
431 return static_cast<CSSStyleSheet *>(m_sheet.get())->isLoading();
432 }
433
434 #if ENABLE(LINK_PREFETCH)
onloadTimerFired(Timer<HTMLLinkElement> * timer)435 void HTMLLinkElement::onloadTimerFired(Timer<HTMLLinkElement>* timer)
436 {
437 ASSERT_UNUSED(timer, timer == &m_onloadTimer);
438 if (m_cachedLinkResource->errorOccurred())
439 dispatchEvent(Event::create(eventNames().errorEvent, false, false));
440 else
441 dispatchEvent(Event::create(eventNames().loadEvent, false, false));
442
443 m_cachedLinkResource->removeClient(this);
444 m_cachedLinkResource = 0;
445 }
446
notifyFinished(CachedResource * resource)447 void HTMLLinkElement::notifyFinished(CachedResource* resource)
448 {
449 m_onloadTimer.startOneShot(0);
450 ASSERT(m_cachedLinkResource.get() == resource);
451 }
452 #endif
453
sheetLoaded()454 bool HTMLLinkElement::sheetLoaded()
455 {
456 if (!isLoading()) {
457 removePendingSheet();
458 return true;
459 }
460 return false;
461 }
462
isURLAttribute(Attribute * attr) const463 bool HTMLLinkElement::isURLAttribute(Attribute *attr) const
464 {
465 return attr->name() == hrefAttr;
466 }
467
href() const468 KURL HTMLLinkElement::href() const
469 {
470 return document()->completeURL(getAttribute(hrefAttr));
471 }
472
rel() const473 String HTMLLinkElement::rel() const
474 {
475 return getAttribute(relAttr);
476 }
477
target() const478 String HTMLLinkElement::target() const
479 {
480 return getAttribute(targetAttr);
481 }
482
type() const483 String HTMLLinkElement::type() const
484 {
485 return getAttribute(typeAttr);
486 }
487
addSubresourceAttributeURLs(ListHashSet<KURL> & urls) const488 void HTMLLinkElement::addSubresourceAttributeURLs(ListHashSet<KURL>& urls) const
489 {
490 HTMLElement::addSubresourceAttributeURLs(urls);
491
492 // Favicons are handled by a special case in LegacyWebArchive::create()
493 if (m_relAttribute.m_isIcon)
494 return;
495
496 if (!m_relAttribute.m_isStyleSheet)
497 return;
498
499 // Append the URL of this link element.
500 addSubresourceURL(urls, href());
501
502 // Walk the URLs linked by the linked-to stylesheet.
503 if (StyleSheet* styleSheet = const_cast<HTMLLinkElement*>(this)->sheet())
504 styleSheet->addSubresourceStyleURLs(urls);
505 }
506
addPendingSheet(PendingSheetType type)507 void HTMLLinkElement::addPendingSheet(PendingSheetType type)
508 {
509 if (type <= m_pendingSheetType)
510 return;
511 m_pendingSheetType = type;
512
513 if (m_pendingSheetType == NonBlocking)
514 return;
515 document()->addPendingSheet();
516 }
517
removePendingSheet()518 void HTMLLinkElement::removePendingSheet()
519 {
520 PendingSheetType type = m_pendingSheetType;
521 m_pendingSheetType = None;
522
523 if (type == None)
524 return;
525 if (type == NonBlocking) {
526 // Document::removePendingSheet() triggers the style selector recalc for blocking sheets.
527 document()->styleSelectorChanged(RecalcStyleImmediately);
528 return;
529 }
530 document()->removePendingSheet();
531 }
532
533 }
534