1 /* 2 * Copyright (C) 1999 Lars Knoll (knoll (at) kde.org) 3 * (C) 1999 Antti Koivisto (koivisto (at) kde.org) 4 * (C) 2001 Dirk Mueller (mueller (at) kde.org) 5 * Copyright (C) 2003, 2006, 2007, 2008, 2009, 2010 Apple Inc. All rights reserved. 6 * Copyright (C) 2009 Rob Buis (rwlbuis (at) 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 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 66 PassRefPtr<HTMLLinkElement> HTMLLinkElement::create(const QualifiedName& tagName, Document* document, bool createdByParser) 67 { 68 return adoptRef(new HTMLLinkElement(tagName, document, createdByParser)); 69 } 70 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 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 123 StyleSheet* HTMLLinkElement::sheet() const 124 { 125 return m_sheet.get(); 126 } 127 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 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 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 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 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 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 354 void HTMLLinkElement::finishParsingChildren() 355 { 356 m_createdByParser = false; 357 HTMLElement::finishParsingChildren(); 358 } 359 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 425 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) 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 447 void HTMLLinkElement::notifyFinished(CachedResource* resource) 448 { 449 m_onloadTimer.startOneShot(0); 450 ASSERT(m_cachedLinkResource.get() == resource); 451 } 452 #endif 453 454 bool HTMLLinkElement::sheetLoaded() 455 { 456 if (!isLoading()) { 457 removePendingSheet(); 458 return true; 459 } 460 return false; 461 } 462 463 bool HTMLLinkElement::isURLAttribute(Attribute *attr) const 464 { 465 return attr->name() == hrefAttr; 466 } 467 468 KURL HTMLLinkElement::href() const 469 { 470 return document()->completeURL(getAttribute(hrefAttr)); 471 } 472 473 String HTMLLinkElement::rel() const 474 { 475 return getAttribute(relAttr); 476 } 477 478 String HTMLLinkElement::target() const 479 { 480 return getAttribute(targetAttr); 481 } 482 483 String HTMLLinkElement::type() const 484 { 485 return getAttribute(typeAttr); 486 } 487 488 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 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 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