| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "core/html/LinkStyle.h" |
| |
| #include "core/css/StyleSheetContents.h" |
| #include "core/frame/LocalFrame.h" |
| #include "core/frame/SubresourceIntegrity.h" |
| #include "core/frame/csp/ContentSecurityPolicy.h" |
| #include "core/html/CrossOriginAttribute.h" |
| #include "core/html/HTMLLinkElement.h" |
| #include "core/loader/FrameLoaderClient.h" |
| #include "core/loader/resource/CSSStyleSheetResource.h" |
| #include "platform/Histogram.h" |
| #include "platform/network/mime/ContentType.h" |
| #include "platform/network/mime/MIMETypeRegistry.h" |
| |
| namespace blink { |
| |
| using namespace HTMLNames; |
| |
| static bool styleSheetTypeIsSupported(const String& type) { |
| String trimmedType = ContentType(type).type(); |
| return trimmedType.isEmpty() || |
| MIMETypeRegistry::isSupportedStyleSheetMIMEType(trimmedType); |
| } |
| |
| LinkStyle* LinkStyle::create(HTMLLinkElement* owner) { |
| return new LinkStyle(owner); |
| } |
| |
| LinkStyle::LinkStyle(HTMLLinkElement* owner) |
| : LinkResource(owner), |
| m_disabledState(Unset), |
| m_pendingSheetType(None), |
| m_loading(false), |
| m_firedLoad(false), |
| m_loadedSheet(false), |
| m_fetchFollowingCORS(false) {} |
| |
| LinkStyle::~LinkStyle() {} |
| |
| Document& LinkStyle::document() { |
| return m_owner->document(); |
| } |
| |
| enum StyleSheetCacheStatus { |
| StyleSheetNewEntry, |
| StyleSheetInDiskCache, |
| StyleSheetInMemoryCache, |
| StyleSheetCacheStatusCount, |
| }; |
| |
| void LinkStyle::setCSSStyleSheet( |
| const String& href, |
| const KURL& baseURL, |
| const String& charset, |
| const CSSStyleSheetResource* cachedStyleSheet) { |
| if (!m_owner->isConnected()) { |
| // While the stylesheet is asynchronously loading, the owner can be |
| // disconnected from a document. |
| // In that case, cancel any processing on the loaded content. |
| m_loading = false; |
| removePendingSheet(); |
| if (m_sheet) |
| clearSheet(); |
| return; |
| } |
| |
| // See the comment in PendingScript.cpp about why this check is necessary |
| // here, instead of in the resource fetcher. https://crbug.com/500701. |
| if (!cachedStyleSheet->errorOccurred() && |
| !m_owner->fastGetAttribute(HTMLNames::integrityAttr).isEmpty() && |
| !cachedStyleSheet->integrityMetadata().isEmpty()) { |
| ResourceIntegrityDisposition disposition = |
| cachedStyleSheet->integrityDisposition(); |
| |
| if (disposition == ResourceIntegrityDisposition::NotChecked && |
| !cachedStyleSheet->loadFailedOrCanceled()) { |
| bool checkResult; |
| |
| // cachedStyleSheet->resourceBuffer() can be nullptr on load success. |
| // If response size == 0. |
| const char* data = nullptr; |
| size_t size = 0; |
| if (cachedStyleSheet->resourceBuffer()) { |
| data = cachedStyleSheet->resourceBuffer()->data(); |
| size = cachedStyleSheet->resourceBuffer()->size(); |
| } |
| checkResult = SubresourceIntegrity::CheckSubresourceIntegrity( |
| *m_owner, data, size, KURL(baseURL, href), *cachedStyleSheet); |
| disposition = checkResult ? ResourceIntegrityDisposition::Passed |
| : ResourceIntegrityDisposition::Failed; |
| |
| // TODO(kouhei): Remove this const_cast crbug.com/653502 |
| const_cast<CSSStyleSheetResource*>(cachedStyleSheet) |
| ->setIntegrityDisposition(disposition); |
| } |
| |
| if (disposition == ResourceIntegrityDisposition::Failed) { |
| m_loading = false; |
| removePendingSheet(); |
| notifyLoadedSheetAndAllCriticalSubresources( |
| Node::ErrorOccurredLoadingSubresource); |
| return; |
| } |
| } |
| |
| CSSParserContext parserContext(m_owner->document(), nullptr, baseURL, |
| charset); |
| |
| DEFINE_STATIC_LOCAL(EnumerationHistogram, restoredCachedStyleSheetHistogram, |
| ("Blink.RestoredCachedStyleSheet", 2)); |
| DEFINE_STATIC_LOCAL( |
| EnumerationHistogram, restoredCachedStyleSheet2Histogram, |
| ("Blink.RestoredCachedStyleSheet2", StyleSheetCacheStatusCount)); |
| |
| if (StyleSheetContents* restoredSheet = |
| const_cast<CSSStyleSheetResource*>(cachedStyleSheet) |
| ->restoreParsedStyleSheet(parserContext)) { |
| DCHECK(restoredSheet->isCacheableForResource()); |
| DCHECK(!restoredSheet->isLoading()); |
| |
| if (m_sheet) |
| clearSheet(); |
| m_sheet = CSSStyleSheet::create(restoredSheet, *m_owner); |
| m_sheet->setMediaQueries(MediaQuerySet::create(m_owner->media())); |
| if (m_owner->isInDocumentTree()) |
| setSheetTitle(m_owner->title()); |
| setCrossOriginStylesheetStatus(m_sheet.get()); |
| |
| m_loading = false; |
| restoredSheet->checkLoaded(); |
| |
| restoredCachedStyleSheetHistogram.count(true); |
| restoredCachedStyleSheet2Histogram.count(StyleSheetInMemoryCache); |
| return; |
| } |
| restoredCachedStyleSheetHistogram.count(false); |
| StyleSheetCacheStatus cacheStatus = cachedStyleSheet->response().wasCached() |
| ? StyleSheetInDiskCache |
| : StyleSheetNewEntry; |
| restoredCachedStyleSheet2Histogram.count(cacheStatus); |
| |
| StyleSheetContents* styleSheet = |
| StyleSheetContents::create(href, parserContext); |
| |
| if (m_sheet) |
| clearSheet(); |
| |
| m_sheet = CSSStyleSheet::create(styleSheet, *m_owner); |
| m_sheet->setMediaQueries(MediaQuerySet::create(m_owner->media())); |
| if (m_owner->isInDocumentTree()) |
| setSheetTitle(m_owner->title()); |
| setCrossOriginStylesheetStatus(m_sheet.get()); |
| |
| styleSheet->parseAuthorStyleSheet(cachedStyleSheet, |
| m_owner->document().getSecurityOrigin()); |
| |
| m_loading = false; |
| styleSheet->notifyLoadedSheet(cachedStyleSheet); |
| styleSheet->checkLoaded(); |
| |
| if (styleSheet->isCacheableForResource()) { |
| const_cast<CSSStyleSheetResource*>(cachedStyleSheet) |
| ->saveParsedStyleSheet(styleSheet); |
| } |
| clearResource(); |
| } |
| |
| bool LinkStyle::sheetLoaded() { |
| if (!styleSheetIsLoading()) { |
| removePendingSheet(); |
| return true; |
| } |
| return false; |
| } |
| |
| void LinkStyle::notifyLoadedSheetAndAllCriticalSubresources( |
| Node::LoadedSheetErrorStatus errorStatus) { |
| if (m_firedLoad) |
| return; |
| m_loadedSheet = (errorStatus == Node::NoErrorLoadingSubresource); |
| if (m_owner) |
| m_owner->scheduleEvent(); |
| m_firedLoad = true; |
| } |
| |
| void LinkStyle::startLoadingDynamicSheet() { |
| DCHECK_LT(m_pendingSheetType, Blocking); |
| addPendingSheet(Blocking); |
| } |
| |
| void LinkStyle::clearSheet() { |
| DCHECK(m_sheet); |
| DCHECK_EQ(m_sheet->ownerNode(), m_owner); |
| m_sheet.release()->clearOwnerNode(); |
| } |
| |
| bool LinkStyle::styleSheetIsLoading() const { |
| if (m_loading) |
| return true; |
| if (!m_sheet) |
| return false; |
| return m_sheet->contents()->isLoading(); |
| } |
| |
| void LinkStyle::addPendingSheet(PendingSheetType type) { |
| if (type <= m_pendingSheetType) |
| return; |
| m_pendingSheetType = type; |
| |
| if (m_pendingSheetType == NonBlocking) |
| return; |
| m_owner->document().styleEngine().addPendingSheet(m_styleEngineContext); |
| } |
| |
| void LinkStyle::removePendingSheet() { |
| DCHECK(m_owner); |
| PendingSheetType type = m_pendingSheetType; |
| m_pendingSheetType = None; |
| |
| if (type == None) |
| return; |
| if (type == NonBlocking) { |
| // Tell StyleEngine to re-compute styleSheets of this m_owner's treescope. |
| m_owner->document().styleEngine().modifiedStyleSheetCandidateNode(*m_owner); |
| return; |
| } |
| |
| m_owner->document().styleEngine().removePendingSheet(*m_owner, |
| m_styleEngineContext); |
| } |
| |
| void LinkStyle::setDisabledState(bool disabled) { |
| LinkStyle::DisabledState oldDisabledState = m_disabledState; |
| m_disabledState = disabled ? Disabled : EnabledViaScript; |
| if (oldDisabledState != m_disabledState) { |
| // 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_owner->relAttribute().isAlternate() && |
| m_disabledState == EnabledViaScript) |
| addPendingSheet(Blocking); |
| |
| // 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_owner->relAttribute().isAlternate() && |
| m_disabledState == EnabledViaScript && oldDisabledState == Disabled) |
| addPendingSheet(Blocking); |
| |
| // If the sheet is already loading just bail. |
| return; |
| } |
| |
| if (m_sheet) { |
| m_sheet->setDisabled(disabled); |
| return; |
| } |
| |
| if (m_disabledState == EnabledViaScript && m_owner->shouldProcessStyle()) |
| process(); |
| } |
| } |
| |
| void LinkStyle::setCrossOriginStylesheetStatus(CSSStyleSheet* sheet) { |
| if (m_fetchFollowingCORS && resource() && !resource()->errorOccurred()) { |
| // Record the security origin the CORS access check succeeded at, if cross |
| // origin. Only origins that are script accessible to it may access the |
| // stylesheet's rules. |
| sheet->setAllowRuleAccessFromOrigin( |
| m_owner->document().getSecurityOrigin()); |
| } |
| m_fetchFollowingCORS = false; |
| } |
| |
| // TODO(yoav): move that logic to LinkLoader |
| LinkStyle::LoadReturnValue LinkStyle::loadStylesheetIfNeeded( |
| const LinkRequestBuilder& builder, |
| const String& type) { |
| if (m_disabledState == Disabled || !m_owner->relAttribute().isStyleSheet() || |
| !styleSheetTypeIsSupported(type) || !shouldLoadResource() || |
| !builder.url().isValid()) |
| return NotNeeded; |
| |
| if (resource()) { |
| removePendingSheet(); |
| clearResource(); |
| clearFetchFollowingCORS(); |
| } |
| |
| if (!m_owner->shouldLoadLink()) |
| return Bail; |
| |
| m_loading = true; |
| |
| String title = m_owner->title(); |
| if (!title.isEmpty() && !m_owner->isAlternate() && |
| m_disabledState != EnabledViaScript && m_owner->isInDocumentTree()) { |
| document().styleEngine().setPreferredStylesheetSetNameIfNotSet( |
| title, StyleEngine::DontUpdateActiveSheets); |
| } |
| |
| bool mediaQueryMatches = true; |
| LocalFrame* frame = loadingFrame(); |
| if (!m_owner->media().isEmpty() && frame) { |
| MediaQuerySet* media = MediaQuerySet::create(m_owner->media()); |
| MediaQueryEvaluator evaluator(frame); |
| mediaQueryMatches = evaluator.eval(media); |
| } |
| |
| // Don't hold up layout tree construction and script execution on |
| // stylesheets that are not needed for the layout at the moment. |
| bool blocking = mediaQueryMatches && !m_owner->isAlternate() && |
| m_owner->isCreatedByParser(); |
| addPendingSheet(blocking ? Blocking : NonBlocking); |
| |
| // Load stylesheets that are not needed for the layout immediately with low |
| // priority. When the link element is created by scripts, load the |
| // stylesheets asynchronously but in high priority. |
| bool lowPriority = !mediaQueryMatches || m_owner->isAlternate(); |
| FetchRequest request = builder.build(lowPriority); |
| CrossOriginAttributeValue crossOrigin = crossOriginAttributeValue( |
| m_owner->fastGetAttribute(HTMLNames::crossoriginAttr)); |
| if (crossOrigin != CrossOriginAttributeNotSet) { |
| request.setCrossOriginAccessControl(document().getSecurityOrigin(), |
| crossOrigin); |
| setFetchFollowingCORS(); |
| } |
| |
| String integrityAttr = m_owner->fastGetAttribute(HTMLNames::integrityAttr); |
| if (!integrityAttr.isEmpty()) { |
| IntegrityMetadataSet metadataSet; |
| SubresourceIntegrity::parseIntegrityAttribute(integrityAttr, metadataSet); |
| request.setIntegrityMetadata(metadataSet); |
| } |
| setResource(CSSStyleSheetResource::fetch(request, document().fetcher())); |
| |
| if (m_loading && !resource()) { |
| // The request may have been denied if (for example) the stylesheet is |
| // local and the document is remote, or if there was a Content Security |
| // Policy Failure. setCSSStyleSheet() can be called synchronuosly in |
| // setResource() and thus resource() is null and |m_loading| is false in |
| // such cases even if the request succeeds. |
| m_loading = false; |
| removePendingSheet(); |
| notifyLoadedSheetAndAllCriticalSubresources( |
| Node::ErrorOccurredLoadingSubresource); |
| } |
| return Loaded; |
| } |
| |
| void LinkStyle::process() { |
| DCHECK(m_owner->shouldProcessStyle()); |
| String type = m_owner->typeValue().lower(); |
| String as = m_owner->asValue().lower(); |
| String media = m_owner->media().lower(); |
| LinkRequestBuilder builder(m_owner); |
| |
| if (m_owner->relAttribute().getIconType() != InvalidIcon && |
| builder.url().isValid() && !builder.url().isEmpty()) { |
| if (!m_owner->shouldLoadLink()) |
| return; |
| if (!document().getSecurityOrigin()->canDisplay(builder.url())) |
| return; |
| if (!document().contentSecurityPolicy()->allowImageFromSource( |
| builder.url())) |
| return; |
| if (document().frame() && document().frame()->loader().client()) { |
| document().frame()->loader().client()->dispatchDidChangeIcons( |
| m_owner->relAttribute().getIconType()); |
| } |
| } |
| |
| if (!m_owner->loadLink(type, as, media, m_owner->referrerPolicy(), |
| builder.url())) |
| return; |
| |
| if (loadStylesheetIfNeeded(builder, type) == NotNeeded && m_sheet) { |
| // we no longer contain a stylesheet, e.g. perhaps rel or type was changed |
| clearSheet(); |
| document().styleEngine().setNeedsActiveStyleUpdate(m_owner->treeScope()); |
| // TODO(rune@opera.com): resolverChanged() can be removed once stylesheet |
| // updates are async. https://crbug.com/567021 |
| document().styleEngine().resolverChanged(FullStyleUpdate); |
| } |
| } |
| |
| void LinkStyle::setSheetTitle( |
| const String& title, |
| StyleEngine::ActiveSheetsUpdate updateActiveSheets) { |
| if (!m_owner->isInDocumentTree() || !m_owner->relAttribute().isStyleSheet()) |
| return; |
| |
| if (m_sheet) |
| m_sheet->setTitle(title); |
| |
| if (title.isEmpty() || !isUnset() || m_owner->isAlternate()) |
| return; |
| |
| KURL href = m_owner->getNonEmptyURLAttribute(hrefAttr); |
| if (href.isValid() && !href.isEmpty()) { |
| document().styleEngine().setPreferredStylesheetSetNameIfNotSet( |
| title, updateActiveSheets); |
| } |
| } |
| |
| void LinkStyle::ownerRemoved() { |
| if (m_sheet) |
| clearSheet(); |
| |
| if (styleSheetIsLoading()) |
| removePendingSheet(); |
| } |
| |
| DEFINE_TRACE(LinkStyle) { |
| visitor->trace(m_sheet); |
| LinkResource::trace(visitor); |
| ResourceOwner<StyleSheetResource>::trace(visitor); |
| } |
| |
| } // namespace blink |