| // 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/strings/string_tokenizer.h" | |
| #include "base/threading/thread_task_runner_handle.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 "nb/memory_scope.h" | |
| #include "url/gurl.h" | |
| namespace cobalt { | |
| namespace dom { | |
| namespace { | |
| 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() { | |
| TRACK_MEMORY_SCOPE("DOM"); | |
| 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); | |
| TRACK_MEMORY_SCOPE("DOM"); | |
| 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) { | |
| scoped_refptr<cssom::CSSStyleSheet> css_style_sheet = | |
| document->html_element_context()->css_parser()->ParseStyleSheet( | |
| content, base::SourceLocation(href(), 1, 1)); | |
| 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 |