blob: 011000b2a8b8839697971f54649bfdc9e86d665c [file] [log] [blame]
// Copyright 2014 The Cobalt Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cobalt/dom/html_link_element.h"
#include <algorithm>
#include <memory>
#include <string>
#include <vector>
#include "base/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_tokenizer.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "cobalt/cssom/css_parser.h"
#include "cobalt/cssom/css_style_sheet.h"
#include "cobalt/dom/document.h"
#include "cobalt/dom/html_element_context.h"
#include "cobalt/dom/window.h"
#include "cobalt/web/csp_delegate.h"
#include "url/gurl.h"
namespace cobalt {
namespace dom {
namespace {
// Constants for parse time histogram. Do not modify these. If you need to
// change these, create a new histogram and new constants.
constexpr size_t kNumParseTimeHistogramBuckets = 100;
constexpr base::TimeDelta kParseTimeHistogramMinTime =
base::TimeDelta::FromMicroseconds(1);
constexpr base::TimeDelta kParseTimeHistogramMaxTime =
base::TimeDelta::FromMilliseconds(10);
bool IsValidRelChar(char const& c) {
return (isalnum(c) || c == '_' || c == '\\' || c == '-');
}
bool IsValidSplashScreenFormat(const std::string& rel) {
base::StringTokenizer tokenizer(rel, "_");
tokenizer.set_options(base::StringTokenizer::RETURN_DELIMS);
bool is_valid_format = true;
while (tokenizer.GetNext()) {
std::string token = tokenizer.token();
if (strcmp(token.c_str(), "splashscreen") == 0) {
is_valid_format = true;
} else {
for (char const& c : token) {
if (!IsValidRelChar(c)) {
return false;
}
}
is_valid_format = false;
}
}
return is_valid_format;
}
web::CspDelegate::ResourceType GetCspResourceTypeForRel(
const std::string& rel) {
if (rel == "stylesheet") {
return web::CspDelegate::kStyle;
} else if (IsValidSplashScreenFormat(rel)) {
return web::CspDelegate::kLocation;
} else {
NOTIMPLEMENTED();
return web::CspDelegate::kImage;
}
}
bool IsRelContentCriticalResource(const std::string& rel) {
return rel == "stylesheet";
}
loader::RequestMode GetRequestMode(
const base::Optional<std::string>& cross_origin_attribute) {
// https://html.spec.whatwg.org/#cors-settings-attribute
if (cross_origin_attribute) {
if (*cross_origin_attribute == "use-credentials") {
return loader::kCORSModeIncludeCredentials;
} else {
// The invalid value default of crossorigin is Anonymous state, leading to
// "same-origin" credentials mode.
return loader::kCORSModeSameOriginCredentials;
}
}
// crossorigin attribute's missing value default is No CORS state, leading to
// "no-cors" request mode.
return loader::kNoCORSMode;
}
} // namespace
// static
const char HTMLLinkElement::kTagName[] = "link";
// static
const std::vector<std::string> HTMLLinkElement::kSupportedRelValues = {
"stylesheet"};
void HTMLLinkElement::OnInsertedIntoDocument() {
HTMLElement::OnInsertedIntoDocument();
if (std::find(kSupportedRelValues.begin(), kSupportedRelValues.end(),
rel()) != kSupportedRelValues.end()) {
Obtain();
} else if (IsValidSplashScreenFormat(rel())) {
Obtain();
} else {
LOG(WARNING) << "<link> has unsupported rel value: " << rel() << ".";
}
}
base::Optional<std::string> HTMLLinkElement::cross_origin() const {
base::Optional<std::string> cross_origin_attribute =
GetAttribute("crossOrigin");
if (cross_origin_attribute &&
(*cross_origin_attribute != "anonymous" &&
*cross_origin_attribute != "use-credentials")) {
return std::string();
}
return cross_origin_attribute;
}
void HTMLLinkElement::set_cross_origin(
const base::Optional<std::string>& value) {
if (value) {
SetAttribute("crossOrigin", *value);
} else {
RemoveAttribute("crossOrigin");
}
}
void HTMLLinkElement::OnRemovedFromDocument() {
HTMLElement::OnRemovedFromDocument();
DCHECK(base::MessageLoop::current());
ReleaseLoader();
if (style_sheet_) {
Document* document = node_document();
if (document) {
document->OnStyleSheetsModified();
}
}
}
void HTMLLinkElement::ResolveAndSetAbsoluteURL() {
// Resolve the URL given by the href attribute, relative to the element.
const GURL& base_url = node_document()->location()->url();
absolute_url_ = base_url.Resolve(href());
LOG_IF(WARNING, !absolute_url_.is_valid())
<< href() << " cannot be resolved based on " << base_url << ".";
}
// Algorithm for Obtain:
// https://www.w3.org/TR/html50/document-metadata.html#concept-link-obtain
void HTMLLinkElement::Obtain() {
TRACE_EVENT0("cobalt::dom", "HTMLLinkElement::Obtain()");
// Custom, not in any spec.
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
Document* document = node_document();
// If the document has no browsing context, do not obtain, parse or apply the
// resource.
if (!document->html_element_context()) {
return;
}
DCHECK(base::MessageLoop::current());
DCHECK(!loader_);
// 1. If the href attribute's value is the empty string, then abort these
// steps.
if (href().empty()) {
return;
}
// 2. Resolve the URL given by the href attribute, relative to the element.
ResolveAndSetAbsoluteURL();
// 3. If the previous step fails, then abort these steps.
if (!absolute_url_.is_valid()) {
return;
}
// 4. Do a potentially CORS-enabled fetch of the resulting absolute URL, with
// the mode being the current state of the element's crossorigin content
// attribute, the origin being the origin of the link element's Document, and
// the default origin behaviour set to taint.
csp::SecurityCallback csp_callback = base::Bind(
&web::CspDelegate::CanLoad, base::Unretained(document->GetCSPDelegate()),
GetCspResourceTypeForRel(rel()));
fetched_last_url_origin_ = loader::Origin();
if (IsRelContentCriticalResource(rel())) {
// The element must delay the load event of the element's document until all
// the attempts to obtain the resource and its critical subresources are
// complete.
document->IncreaseLoadingCounter();
}
request_mode_ = GetRequestMode(GetAttribute("crossOrigin"));
DCHECK(!loader_);
loader::Origin origin = document->location()
? document->location()->GetOriginAsObject()
: loader::Origin();
disk_cache::ResourceType type;
if (rel() == "stylesheet") {
type = disk_cache::kCSS;
} else if (IsValidSplashScreenFormat(rel())) {
type = disk_cache::kSplashScreen;
} else {
LOG(WARNING) << "<link> has unsupported rel value: " << rel() << ".";
NOTIMPLEMENTED();
return;
}
loader_ = html_element_context()->loader_factory()->CreateLinkLoader(
absolute_url_, origin, csp_callback, request_mode_, type,
base::Bind(&HTMLLinkElement::OnContentProduced, base::Unretained(this)),
base::Bind(&HTMLLinkElement::OnLoadingComplete, base::Unretained(this)));
}
void HTMLLinkElement::OnContentProduced(const loader::Origin& last_url_origin,
std::unique_ptr<std::string> content) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DCHECK(content);
TRACE_EVENT0("cobalt::dom", "HTMLLinkElement::OnContentProduced()");
// Get resource's final destination url from loader.
fetched_last_url_origin_ = last_url_origin;
Document* document = node_document();
if (rel() == "stylesheet") {
OnStylesheetLoaded(document, *content);
} else if (IsValidSplashScreenFormat(rel())) {
OnSplashscreenLoaded(document, *content);
} else {
NOTIMPLEMENTED();
return;
}
// Once the attempts to obtain the resource and its critical subresources
// are complete, the user agent must, if the loads were successful, queue a
// task to fire a simple event named load at the link element, or, if the
// resource or one of its critical subresources failed to completely load
// for any reason (e.g. DNS error, HTTP 404 response, a connection being
// prematurely closed, unsupported Content-Type), queue a task to fire a
// simple event named error at the link element.
PostToDispatchEventName(FROM_HERE, base::Tokens::load());
if (IsRelContentCriticalResource(rel())) {
// The element must delay the load event of the element's document until all
// the attempts to obtain the resource and its critical subresources are
// complete.
document->DecreaseLoadingCounterAndMaybeDispatchLoadEvent();
}
}
void HTMLLinkElement::OnLoadingComplete(
const base::Optional<std::string>& error) {
// GetLoadTimingInfo and create resource timing before loader released.
GetLoadTimingInfoAndCreateResourceTiming();
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::Bind(&HTMLLinkElement::ReleaseLoader, this));
if (!error) return;
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
TRACE_EVENT0("cobalt::dom", "HTMLLinkElement::OnLoadingComplete()");
LOG(ERROR) << *error;
// Once the attempts to obtain the resource and its critical subresources are
// complete, the user agent must, if the loads were successful, queue a task
// to fire a simple event named load at the link element, or, if the resource
// or one of its critical subresources failed to completely load for any
// reason (e.g. DNS error, HTTP 404 response, a connection being prematurely
// closed, unsupported Content-Type), queue a task to fire a simple event
// named error at the link element.
PostToDispatchEventName(FROM_HERE, base::Tokens::error());
if (IsRelContentCriticalResource(rel())) {
// The element must delay the load event of the element's document until all
// the attempts to obtain the resource and its critical subresources are
// complete.
node_document()->DecreaseLoadingCounterAndMaybeDispatchLoadEvent();
}
}
void HTMLLinkElement::OnSplashscreenLoaded(Document* document,
const std::string& content) {
scoped_refptr<Window> window = document->window();
std::string link = rel();
size_t last_underscore = link.find_last_of("_");
base::Optional<std::string> topic;
if (last_underscore != std::string::npos) {
topic = link.substr(0, last_underscore);
}
window->CacheSplashScreen(content, topic);
}
void HTMLLinkElement::OnStylesheetLoaded(Document* document,
const std::string& content) {
auto before_parse_micros = SbTimeGetMonotonicNow();
scoped_refptr<cssom::CSSStyleSheet> css_style_sheet =
document->html_element_context()->css_parser()->ParseStyleSheet(
content, base::SourceLocation(href(), 1, 1));
auto after_parse_micros = SbTimeGetMonotonicNow();
auto css_kb = content.length() / 1000;
// Only measure non-trivial CSS sizes and ignore non-HTTP schemes (e.g.,
// file://), which are primarily used for debug purposes.
if (css_kb > 0 && absolute_url_.SchemeIsHTTPOrHTTPS()) {
// Get parse time normalized by byte size, see:
// go/cobalt-js-css-parsing-metrics.
auto micros_per_kb = (after_parse_micros - before_parse_micros) / css_kb;
UMA_HISTOGRAM_CUSTOM_MICROSECONDS_TIMES(
"Cobalt.DOM.CSS.Link.ParseTimeMicrosPerKB",
base::TimeDelta::FromMicroseconds(micros_per_kb),
kParseTimeHistogramMinTime, kParseTimeHistogramMaxTime,
kNumParseTimeHistogramBuckets);
}
css_style_sheet->SetLocationUrl(absolute_url_);
// If not loading from network-fetched resources or fetched resource is same
// origin as the document, set origin-clean flag to true.
if (request_mode_ != loader::kNoCORSMode || !loader_ ||
document->location()->url().SchemeIsFile() ||
(fetched_last_url_origin_ == document->location()->GetOriginAsObject())) {
css_style_sheet->SetOriginClean(true);
}
style_sheet_ = css_style_sheet;
document->OnStyleSheetsModified();
}
void HTMLLinkElement::ReleaseLoader() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
loader_.reset();
}
void HTMLLinkElement::CollectStyleSheet(
cssom::StyleSheetVector* style_sheets) const {
if (style_sheet_) {
style_sheets->push_back(style_sheet_);
}
}
void HTMLLinkElement::GetLoadTimingInfoAndCreateResourceTiming() {
if (html_element_context()->performance() == nullptr) return;
if (loader_) {
html_element_context()->performance()->CreatePerformanceResourceTiming(
loader_->get_load_timing_info(), kTagName, absolute_url_.spec());
}
}
} // namespace dom
} // namespace cobalt