/* * Copyright (C) 1999 Lars Knoll (knoll@kde.org) * (C) 1999 Antti Koivisto (koivisto@kde.org) * (C) 2001 Dirk Mueller (mueller@kde.org) * Copyright (C) 2003-2017 Apple Inc. All rights reserved. * Copyright (C) 2009 Rob Buis (rwlbuis@gmail.com) * Copyright (C) 2011 Google Inc. All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "config.h" #include "HTMLLinkElement.h" #include "Attribute.h" #include "CachedCSSStyleSheet.h" #include "CachedResource.h" #include "CachedResourceLoader.h" #include "CachedResourceRequest.h" #include "ContentSecurityPolicy.h" #include "CrossOriginAccessControl.h" #include "DOMTokenList.h" #include "DefaultResourceLoadPriority.h" #include "Document.h" #include "Event.h" #include "EventNames.h" #include "EventSender.h" #include "Frame.h" #include "FrameLoader.h" #include "FrameLoaderClient.h" #include "FrameTree.h" #include "FrameView.h" #include "HTMLAnchorElement.h" #include "HTMLNames.h" #include "HTMLParserIdioms.h" #include "Logging.h" #include "MediaList.h" #include "MediaQueryEvaluator.h" #include "MediaQueryParser.h" #include "MouseEvent.h" #include "ParsedContentType.h" #include "RenderStyle.h" #include "SecurityOrigin.h" #include "Settings.h" #include "StyleInheritedData.h" #include "StyleResolveForDocument.h" #include "StyleScope.h" #include "StyleSheetContents.h" #include "SubresourceIntegrity.h" #include #include #include #include #include namespace WebCore { WTF_MAKE_ISO_ALLOCATED_IMPL(HTMLLinkElement); using namespace HTMLNames; static LinkEventSender& linkLoadEventSender() { static NeverDestroyed sharedLoadEventSender(eventNames().loadEvent); return sharedLoadEventSender; } static LinkEventSender& linkErrorEventSender() { static NeverDestroyed sharedErrorEventSender(eventNames().errorEvent); return sharedErrorEventSender; } inline HTMLLinkElement::HTMLLinkElement(const QualifiedName& tagName, Document& document, bool createdByParser) : HTMLElement(tagName, document) , m_linkLoader(*this) , m_disabledState(Unset) , m_loading(false) , m_createdByParser(createdByParser) , m_firedLoad(false) , m_loadedResource(false) , m_isHandlingBeforeLoad(false) , m_allowPrefetchLoadAndErrorForTesting(false) , m_pendingSheetType(Unknown) { ASSERT(hasTagName(linkTag)); } Ref HTMLLinkElement::create(const QualifiedName& tagName, Document& document, bool createdByParser) { return adoptRef(*new HTMLLinkElement(tagName, document, createdByParser)); } HTMLLinkElement::~HTMLLinkElement() { if (m_sheet) m_sheet->clearOwnerNode(); if (m_cachedSheet) m_cachedSheet->removeClient(*this); if (m_styleScope) m_styleScope->removeStyleSheetCandidateNode(*this); linkLoadEventSender().cancelEvent(*this); linkErrorEventSender().cancelEvent(*this); } void HTMLLinkElement::setDisabledState(bool disabled) { DisabledState oldDisabledState = m_disabledState; m_disabledState = disabled ? Disabled : EnabledViaScript; if (oldDisabledState == m_disabledState) return; ASSERT(isConnected() || !styleSheetIsLoading()); if (!isConnected()) return; // If we change the disabled state while the sheet is still loading, then we have to // perform three checks: if (styleSheetIsLoading()) { // Check #1: The sheet becomes disabled while loading. if (m_disabledState == Disabled) removePendingSheet(); // Check #2: An alternate sheet becomes enabled while it is still loading. if (m_relAttribute.isAlternate && m_disabledState == EnabledViaScript) addPendingSheet(ActiveSheet); // Check #3: A main sheet becomes enabled while it was still loading and // after it was disabled via script. It takes really terrible code to make this // happen (a double toggle for no reason essentially). This happens on // virtualplastic.net, which manages to do about 12 enable/disables on only 3 // sheets. :) if (!m_relAttribute.isAlternate && m_disabledState == EnabledViaScript && oldDisabledState == Disabled) addPendingSheet(ActiveSheet); // If the sheet is already loading just bail. return; } // Load the sheet, since it's never been loaded before. if (!m_sheet && m_disabledState == EnabledViaScript) process(); else { ASSERT(m_styleScope); m_styleScope->didChangeActiveStyleSheetCandidates(); } } void HTMLLinkElement::parseAttribute(const QualifiedName& name, const AtomString& value) { if (name == relAttr) { m_relAttribute = LinkRelAttribute(document(), value); if (m_relList) m_relList->associatedAttributeValueChanged(value); process(); return; } if (name == hrefAttr) { process(); return; } if (name == typeAttr) { m_type = value; process(); return; } if (name == sizesAttr) { if (m_sizes) m_sizes->associatedAttributeValueChanged(value); process(); return; } if (name == mediaAttr) { m_media = value.string().convertToASCIILowercase(); process(); if (m_sheet && !isDisabled()) m_styleScope->didChangeActiveStyleSheetCandidates(); return; } if (name == disabledAttr) { setDisabledState(!value.isNull()); return; } if (name == titleAttr) { if (m_sheet && !isInShadowTree()) m_sheet->setTitle(value); return; } HTMLElement::parseAttribute(name, value); } bool HTMLLinkElement::shouldLoadLink() { Ref originalDocument = document(); if (!dispatchBeforeLoadEvent(getNonEmptyURLAttribute(hrefAttr).string())) return false; // A beforeload handler might have removed us from the document or changed the document. if (!isConnected() || &document() != originalDocument.ptr()) return false; return true; } void HTMLLinkElement::setCrossOrigin(const AtomString& value) { setAttributeWithoutSynchronization(crossoriginAttr, value); } String HTMLLinkElement::crossOrigin() const { return parseCORSSettingsAttribute(attributeWithoutSynchronization(crossoriginAttr)); } void HTMLLinkElement::setAs(const AtomString& value) { setAttributeWithoutSynchronization(asAttr, value); } String HTMLLinkElement::as() const { String as = attributeWithoutSynchronization(asAttr); if (equalLettersIgnoringASCIICase(as, "fetch") || equalLettersIgnoringASCIICase(as, "image") || equalLettersIgnoringASCIICase(as, "script") || equalLettersIgnoringASCIICase(as, "style") || (document().settings().mediaPreloadingEnabled() && (equalLettersIgnoringASCIICase(as, "video") || equalLettersIgnoringASCIICase(as, "audio"))) || equalLettersIgnoringASCIICase(as, "track") || equalLettersIgnoringASCIICase(as, "font")) return as.convertToASCIILowercase(); return String(); } void HTMLLinkElement::process() { if (!isConnected()) { ASSERT(!m_sheet); return; } // Prevent recursive loading of link. if (m_isHandlingBeforeLoad) return; URL url = getNonEmptyURLAttribute(hrefAttr); LinkLoadParameters params { m_relAttribute, url, attributeWithoutSynchronization(asAttr), attributeWithoutSynchronization(mediaAttr), attributeWithoutSynchronization(typeAttr), attributeWithoutSynchronization(crossoriginAttr), attributeWithoutSynchronization(imagesrcsetAttr), attributeWithoutSynchronization(imagesizesAttr), referrerPolicy(), }; m_linkLoader.loadLink(params, document()); bool treatAsStyleSheet = false; if (m_relAttribute.isStyleSheet) { if (m_type.isNull()) treatAsStyleSheet = true; else if (auto parsedContentType = ParsedContentType::create(m_type)) treatAsStyleSheet = equalLettersIgnoringASCIICase(parsedContentType->mimeType(), "text/css"); } if (!treatAsStyleSheet) treatAsStyleSheet = document().settings().treatsAnyTextCSSLinkAsStylesheet() && m_type.containsIgnoringASCIICase("text/css"); LOG_WITH_STREAM(StyleSheets, stream << "HTMLLinkElement " << this << " process() - treatAsStyleSheet " << treatAsStyleSheet); if (m_disabledState != Disabled && treatAsStyleSheet && document().frame() && url.isValid()) { String charset = attributeWithoutSynchronization(charsetAttr); if (!TextEncoding { charset }.isValid()) charset = document().charset(); if (m_cachedSheet) { removePendingSheet(); m_cachedSheet->removeClient(*this); m_cachedSheet = nullptr; } { bool previous = m_isHandlingBeforeLoad; m_isHandlingBeforeLoad = true; auto scopeExit = makeScopeExit([&] { m_isHandlingBeforeLoad = previous; }); if (!shouldLoadLink()) return; } m_loading = true; bool mediaQueryMatches = true; if (!m_media.isEmpty()) { std::optional documentStyle; if (document().hasLivingRenderTree()) documentStyle = Style::resolveForDocument(document()); auto media = MediaQuerySet::create(m_media, MediaQueryParserContext(document())); LOG(MediaQueries, "HTMLLinkElement::process evaluating queries"); mediaQueryMatches = MediaQueryEvaluator { document().frame()->view()->mediaType(), document(), documentStyle ? &*documentStyle : nullptr }.evaluate(media.get()); } // Don't hold up render tree construction and script execution on stylesheets // that are not needed for the rendering at the moment. bool isActive = mediaQueryMatches && !isAlternate(); addPendingSheet(isActive ? ActiveSheet : InactiveSheet); // Load stylesheets that are not needed for the rendering immediately with low priority. std::optional priority; if (!isActive) priority = DefaultResourceLoadPriority::inactiveStyleSheet; if (document().settings().subresourceIntegrityEnabled()) m_integrityMetadataForPendingSheetRequest = attributeWithoutSynchronization(HTMLNames::integrityAttr); ResourceLoaderOptions options = CachedResourceLoader::defaultCachedResourceOptions(); options.sameOriginDataURLFlag = SameOriginDataURLFlag::Set; if (document().contentSecurityPolicy()->allowStyleWithNonce(attributeWithoutSynchronization(HTMLNames::nonceAttr))) options.contentSecurityPolicyImposition = ContentSecurityPolicyImposition::SkipPolicyCheck; options.integrity = m_integrityMetadataForPendingSheetRequest; options.referrerPolicy = params.referrerPolicy; auto request = createPotentialAccessControlRequest(WTFMove(url), WTFMove(options), document(), crossOrigin()); request.setPriority(WTFMove(priority)); request.setCharset(WTFMove(charset)); request.setInitiator(*this); ASSERT_WITH_SECURITY_IMPLICATION(!m_cachedSheet); m_cachedSheet = document().cachedResourceLoader().requestCSSStyleSheet(WTFMove(request)).value_or(nullptr); if (m_cachedSheet) m_cachedSheet->addClient(*this); else { // The request may have been denied if (for example) the stylesheet is local and the document is remote. m_loading = false; sheetLoaded(); notifyLoadedSheetAndAllCriticalSubresources(true); } } else if (m_sheet) { // we no longer contain a stylesheet, e.g. perhaps rel or type was changed clearSheet(); m_styleScope->didChangeActiveStyleSheetCandidates(); } } void HTMLLinkElement::clearSheet() { ASSERT(m_sheet); ASSERT(m_sheet->ownerNode() == this); m_sheet->clearOwnerNode(); m_sheet = nullptr; } Node::InsertedIntoAncestorResult HTMLLinkElement::insertedIntoAncestor(InsertionType insertionType, ContainerNode& parentOfInsertedTree) { HTMLElement::insertedIntoAncestor(insertionType, parentOfInsertedTree); if (!insertionType.connectedToDocument) return InsertedIntoAncestorResult::Done; m_styleScope = &Style::Scope::forNode(*this); m_styleScope->addStyleSheetCandidateNode(*this, m_createdByParser); return InsertedIntoAncestorResult::NeedsPostInsertionCallback; } void HTMLLinkElement::didFinishInsertingNode() { process(); } void HTMLLinkElement::removedFromAncestor(RemovalType removalType, ContainerNode& oldParentOfRemovedTree) { HTMLElement::removedFromAncestor(removalType, oldParentOfRemovedTree); if (!removalType.disconnectedFromDocument) return; m_linkLoader.cancelLoad(); bool wasLoading = styleSheetIsLoading(); if (m_sheet) clearSheet(); if (wasLoading) removePendingSheet(); if (m_styleScope) { m_styleScope->removeStyleSheetCandidateNode(*this); m_styleScope = nullptr; } } void HTMLLinkElement::finishParsingChildren() { m_createdByParser = false; HTMLElement::finishParsingChildren(); } void HTMLLinkElement::initializeStyleSheet(Ref&& styleSheet, const CachedCSSStyleSheet& cachedStyleSheet, MediaQueryParserContext context) { // FIXME: originClean should be turned to false except if fetch mode is CORS. std::optional originClean; if (cachedStyleSheet.options().mode == FetchOptions::Mode::Cors) originClean = cachedStyleSheet.isCORSSameOrigin(); m_sheet = CSSStyleSheet::create(WTFMove(styleSheet), *this, originClean); m_sheet->setMediaQueries(MediaQuerySet::create(m_media, context)); if (!isInShadowTree()) m_sheet->setTitle(title()); if (!m_sheet->canAccessRules()) m_sheet->contents().setAsOpaque(); } void HTMLLinkElement::setCSSStyleSheet(const String& href, const URL& baseURL, const String& charset, const CachedCSSStyleSheet* cachedStyleSheet) { if (!isConnected()) { ASSERT(!m_sheet); return; } auto frame = makeRefPtr(document().frame()); if (!frame) return; // Completing the sheet load may cause scripts to execute. Ref protectedThis(*this); if (!cachedStyleSheet->errorOccurred() && !matchIntegrityMetadata(*cachedStyleSheet, m_integrityMetadataForPendingSheetRequest)) { document().addConsoleMessage(MessageSource::Security, MessageLevel::Error, makeString("Cannot load stylesheet ", integrityMismatchDescription(*cachedStyleSheet, m_integrityMetadataForPendingSheetRequest))); m_loading = false; sheetLoaded(); notifyLoadedSheetAndAllCriticalSubresources(true); return; } CSSParserContext parserContext(document(), baseURL, charset); auto cachePolicy = frame->loader().subresourceCachePolicy(baseURL); if (auto restoredSheet = const_cast(cachedStyleSheet)->restoreParsedStyleSheet(parserContext, cachePolicy, frame->loader())) { ASSERT(restoredSheet->isCacheable()); ASSERT(!restoredSheet->isLoading()); initializeStyleSheet(restoredSheet.releaseNonNull(), *cachedStyleSheet, MediaQueryParserContext(document())); m_loading = false; sheetLoaded(); notifyLoadedSheetAndAllCriticalSubresources(false); return; } auto styleSheet = StyleSheetContents::create(href, parserContext); initializeStyleSheet(styleSheet.copyRef(), *cachedStyleSheet, MediaQueryParserContext(document())); // FIXME: Set the visibility option based on m_sheet being clean or not. // Best approach might be to set it on the style sheet content itself or its context parser otherwise. if (!styleSheet.get().parseAuthorStyleSheet(cachedStyleSheet, &document().securityOrigin())) { m_loading = false; sheetLoaded(); notifyLoadedSheetAndAllCriticalSubresources(true); return; } m_loading = false; styleSheet.get().notifyLoadedSheet(cachedStyleSheet); styleSheet.get().checkLoaded(); if (styleSheet.get().isCacheable()) const_cast(cachedStyleSheet)->saveParsedStyleSheet(WTFMove(styleSheet)); } bool HTMLLinkElement::styleSheetIsLoading() const { if (m_loading) return true; if (!m_sheet) return false; return m_sheet->contents().isLoading(); } DOMTokenList& HTMLLinkElement::sizes() { if (!m_sizes) m_sizes = makeUnique(*this, sizesAttr); return *m_sizes; } void HTMLLinkElement::linkLoaded() { m_loadedResource = true; if (!m_relAttribute.isLinkPrefetch || m_allowPrefetchLoadAndErrorForTesting) linkLoadEventSender().dispatchEventSoon(*this); } void HTMLLinkElement::linkLoadingErrored() { if (!m_relAttribute.isLinkPrefetch || m_allowPrefetchLoadAndErrorForTesting) linkErrorEventSender().dispatchEventSoon(*this); } bool HTMLLinkElement::sheetLoaded() { if (!styleSheetIsLoading()) { removePendingSheet(); return true; } return false; } void HTMLLinkElement::dispatchPendingLoadEvents(Page* page) { linkLoadEventSender().dispatchPendingEvents(page); } void HTMLLinkElement::dispatchPendingEvent(LinkEventSender* eventSender) { ASSERT_UNUSED(eventSender, eventSender == &linkLoadEventSender() || eventSender == &linkErrorEventSender()); if (m_loadedResource) dispatchEvent(Event::create(eventNames().loadEvent, Event::CanBubble::No, Event::IsCancelable::No)); else dispatchEvent(Event::create(eventNames().errorEvent, Event::CanBubble::No, Event::IsCancelable::No)); } DOMTokenList& HTMLLinkElement::relList() { if (!m_relList) m_relList = makeUnique(*this, HTMLNames::relAttr, [](Document& document, StringView token) { return LinkRelAttribute::isSupported(document, token); }); return *m_relList; } void HTMLLinkElement::notifyLoadedSheetAndAllCriticalSubresources(bool errorOccurred) { if (m_firedLoad) return; m_loadedResource = !errorOccurred; linkLoadEventSender().dispatchEventSoon(*this); m_firedLoad = true; } void HTMLLinkElement::startLoadingDynamicSheet() { // We don't support multiple active sheets. ASSERT(m_pendingSheetType < ActiveSheet); addPendingSheet(ActiveSheet); } bool HTMLLinkElement::isURLAttribute(const Attribute& attribute) const { return attribute.name().localName() == hrefAttr || HTMLElement::isURLAttribute(attribute); } URL HTMLLinkElement::href() const { return document().completeURL(attributeWithoutSynchronization(hrefAttr)); } const AtomString& HTMLLinkElement::rel() const { return attributeWithoutSynchronization(relAttr); } String HTMLLinkElement::target() const { return attributeWithoutSynchronization(targetAttr); } const AtomString& HTMLLinkElement::type() const { return attributeWithoutSynchronization(typeAttr); } std::optional HTMLLinkElement::iconType() const { return m_relAttribute.iconType; } void HTMLLinkElement::addSubresourceAttributeURLs(ListHashSet& urls) const { HTMLElement::addSubresourceAttributeURLs(urls); // Favicons are handled by a special case in LegacyWebArchive::create() if (m_relAttribute.iconType) return; if (!m_relAttribute.isStyleSheet) return; // Append the URL of this link element. addSubresourceURL(urls, href()); if (auto styleSheet = makeRefPtr(this->sheet())) { styleSheet->contents().traverseSubresources([&] (auto& resource) { urls.add(resource.url()); return false; }); } } void HTMLLinkElement::addPendingSheet(PendingSheetType type) { if (type <= m_pendingSheetType) return; m_pendingSheetType = type; if (m_pendingSheetType == InactiveSheet) return; ASSERT(m_styleScope); m_styleScope->addPendingSheet(*this); } void HTMLLinkElement::removePendingSheet() { PendingSheetType type = m_pendingSheetType; m_pendingSheetType = Unknown; if (type == Unknown) return; ASSERT(m_styleScope); if (type == InactiveSheet) { // Document just needs to know about the sheet for exposure through document.styleSheets m_styleScope->didChangeActiveStyleSheetCandidates(); return; } m_styleScope->removePendingSheet(*this); } void HTMLLinkElement::setReferrerPolicyForBindings(const AtomString& value) { setAttributeWithoutSynchronization(referrerpolicyAttr, value); } String HTMLLinkElement::referrerPolicyForBindings() const { return referrerPolicyToString(referrerPolicy()); } ReferrerPolicy HTMLLinkElement::referrerPolicy() const { if (document().settings().referrerPolicyAttributeEnabled()) return parseReferrerPolicy(attributeWithoutSynchronization(referrerpolicyAttr), ReferrerPolicySource::ReferrerPolicyAttribute).value_or(ReferrerPolicy::EmptyString); return ReferrerPolicy::EmptyString; } String HTMLLinkElement::debugDescription() const { return makeString(HTMLElement::debugDescription(), ' ', type(), ' ', href().string()); } } // namespace WebCore