blob: 9460d3c1025bc6838149dbc668a2e7841dcaf4e8 [file] [log] [blame]
// 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