1/*
2 * Copyright (C) 1999 Lars Knoll (knoll@kde.org)
3 * (C) 1999 Antti Koivisto (koivisto@kde.org)
4 * (C) 2001 Dirk Mueller (mueller@kde.org)
5 * Copyright (C) 2003-2017 Apple Inc. All rights reserved.
6 * Copyright (C) 2009 Rob Buis (rwlbuis@gmail.com)
7 * Copyright (C) 2011 Google Inc. All rights reserved.
8 *
9 * This library is free software; you can redistribute it and/or
10 * modify it under the terms of the GNU Library General Public
11 * License as published by the Free Software Foundation; either
12 * version 2 of the License, or (at your option) any later version.
13 *
14 * This library is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 * Library General Public License for more details.
18 *
19 * You should have received a copy of the GNU Library General Public License
20 * along with this library; see the file COPYING.LIB. If not, write to
21 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
22 * Boston, MA 02110-1301, USA.
23 */
24
25#include "config.h"
26#include "HTMLLinkElement.h"
27
28#include "Attribute.h"
29#include "CachedCSSStyleSheet.h"
30#include "CachedResource.h"
31#include "CachedResourceLoader.h"
32#include "CachedResourceRequest.h"
33#include "ContentSecurityPolicy.h"
34#include "CrossOriginAccessControl.h"
35#include "DOMTokenList.h"
36#include "Document.h"
37#include "Event.h"
38#include "EventNames.h"
39#include "EventSender.h"
40#include "Frame.h"
41#include "FrameLoader.h"
42#include "FrameLoaderClient.h"
43#include "FrameTree.h"
44#include "FrameView.h"
45#include "HTMLAnchorElement.h"
46#include "HTMLNames.h"
47#include "HTMLParserIdioms.h"
48#include "Logging.h"
49#include "MediaList.h"
50#include "MediaQueryEvaluator.h"
51#include "MediaQueryParser.h"
52#include "MouseEvent.h"
53#include "RenderStyle.h"
54#include "RuntimeEnabledFeatures.h"
55#include "SecurityOrigin.h"
56#include "Settings.h"
57#include "StyleInheritedData.h"
58#include "StyleResolveForDocument.h"
59#include "StyleScope.h"
60#include "StyleSheetContents.h"
61#include "SubresourceIntegrity.h"
62#include <wtf/IsoMallocInlines.h>
63#include <wtf/Ref.h>
64#include <wtf/SetForScope.h>
65#include <wtf/StdLibExtras.h>
66
67namespace WebCore {
68
69WTF_MAKE_ISO_ALLOCATED_IMPL(HTMLLinkElement);
70
71using namespace HTMLNames;
72
73static LinkEventSender& linkLoadEventSender()
74{
75 static NeverDestroyed<LinkEventSender> sharedLoadEventSender(eventNames().loadEvent);
76 return sharedLoadEventSender;
77}
78
79static LinkEventSender& linkErrorEventSender()
80{
81 static NeverDestroyed<LinkEventSender> sharedErrorEventSender(eventNames().errorEvent);
82 return sharedErrorEventSender;
83}
84
85inline HTMLLinkElement::HTMLLinkElement(const QualifiedName& tagName, Document& document, bool createdByParser)
86 : HTMLElement(tagName, document)
87 , m_linkLoader(*this)
88 , m_disabledState(Unset)
89 , m_loading(false)
90 , m_createdByParser(createdByParser)
91 , m_firedLoad(false)
92 , m_loadedResource(false)
93 , m_pendingSheetType(Unknown)
94{
95 ASSERT(hasTagName(linkTag));
96}
97
98Ref<HTMLLinkElement> HTMLLinkElement::create(const QualifiedName& tagName, Document& document, bool createdByParser)
99{
100 return adoptRef(*new HTMLLinkElement(tagName, document, createdByParser));
101}
102
103HTMLLinkElement::~HTMLLinkElement()
104{
105 if (m_sheet)
106 m_sheet->clearOwnerNode();
107
108 if (m_cachedSheet)
109 m_cachedSheet->removeClient(*this);
110
111 if (m_styleScope)
112 m_styleScope->removeStyleSheetCandidateNode(*this);
113
114 linkLoadEventSender().cancelEvent(*this);
115 linkErrorEventSender().cancelEvent(*this);
116}
117
118void HTMLLinkElement::setDisabledState(bool disabled)
119{
120 DisabledState oldDisabledState = m_disabledState;
121 m_disabledState = disabled ? Disabled : EnabledViaScript;
122 if (oldDisabledState == m_disabledState)
123 return;
124
125 ASSERT(isConnected() || !styleSheetIsLoading());
126 if (!isConnected())
127 return;
128
129 // If we change the disabled state while the sheet is still loading, then we have to
130 // perform three checks:
131 if (styleSheetIsLoading()) {
132 // Check #1: The sheet becomes disabled while loading.
133 if (m_disabledState == Disabled)
134 removePendingSheet();
135
136 // Check #2: An alternate sheet becomes enabled while it is still loading.
137 if (m_relAttribute.isAlternate && m_disabledState == EnabledViaScript)
138 addPendingSheet(ActiveSheet);
139
140 // Check #3: A main sheet becomes enabled while it was still loading and
141 // after it was disabled via script. It takes really terrible code to make this
142 // happen (a double toggle for no reason essentially). This happens on
143 // virtualplastic.net, which manages to do about 12 enable/disables on only 3
144 // sheets. :)
145 if (!m_relAttribute.isAlternate && m_disabledState == EnabledViaScript && oldDisabledState == Disabled)
146 addPendingSheet(ActiveSheet);
147
148 // If the sheet is already loading just bail.
149 return;
150 }
151
152 // Load the sheet, since it's never been loaded before.
153 if (!m_sheet && m_disabledState == EnabledViaScript)
154 process();
155 else {
156 ASSERT(m_styleScope);
157 m_styleScope->didChangeActiveStyleSheetCandidates();
158 }
159}
160
161void HTMLLinkElement::parseAttribute(const QualifiedName& name, const AtomicString& value)
162{
163 if (name == relAttr) {
164 m_relAttribute = LinkRelAttribute(document(), value);
165 if (m_relList)
166 m_relList->associatedAttributeValueChanged(value);
167 process();
168 return;
169 }
170 if (name == hrefAttr) {
171 bool wasLink = isLink();
172 setIsLink(!value.isNull() && !shouldProhibitLinks(this));
173 if (wasLink != isLink())
174 invalidateStyleForSubtree();
175 process();
176 return;
177 }
178 if (name == typeAttr) {
179 m_type = value;
180 process();
181 return;
182 }
183 if (name == sizesAttr) {
184 if (m_sizes)
185 m_sizes->associatedAttributeValueChanged(value);
186 process();
187 return;
188 }
189 if (name == mediaAttr) {
190 m_media = value.string().convertToASCIILowercase();
191 process();
192 if (m_sheet && !isDisabled())
193 m_styleScope->didChangeActiveStyleSheetCandidates();
194 return;
195 }
196 if (name == disabledAttr) {
197 setDisabledState(!value.isNull());
198 return;
199 }
200 if (name == titleAttr) {
201 if (m_sheet && !isInShadowTree())
202 m_sheet->setTitle(value);
203 return;
204 }
205 HTMLElement::parseAttribute(name, value);
206}
207
208bool HTMLLinkElement::shouldLoadLink()
209{
210 Ref<Document> originalDocument = document();
211 if (!dispatchBeforeLoadEvent(getNonEmptyURLAttribute(hrefAttr)))
212 return false;
213 // A beforeload handler might have removed us from the document or changed the document.
214 if (!isConnected() || &document() != originalDocument.ptr())
215 return false;
216 return true;
217}
218
219void HTMLLinkElement::setCrossOrigin(const AtomicString& value)
220{
221 setAttributeWithoutSynchronization(crossoriginAttr, value);
222}
223
224String HTMLLinkElement::crossOrigin() const
225{
226 return parseCORSSettingsAttribute(attributeWithoutSynchronization(crossoriginAttr));
227}
228
229void HTMLLinkElement::setAs(const AtomicString& value)
230{
231 setAttributeWithoutSynchronization(asAttr, value);
232}
233
234String HTMLLinkElement::as() const
235{
236 String as = attributeWithoutSynchronization(asAttr);
237 if (equalLettersIgnoringASCIICase(as, "fetch")
238 || equalLettersIgnoringASCIICase(as, "image")
239 || equalLettersIgnoringASCIICase(as, "script")
240 || equalLettersIgnoringASCIICase(as, "style")
241 || (RuntimeEnabledFeatures::sharedFeatures().mediaPreloadingEnabled()
242 && (equalLettersIgnoringASCIICase(as, "video")
243 || equalLettersIgnoringASCIICase(as, "audio")))
244#if ENABLE(VIDEO_TRACK)
245 || equalLettersIgnoringASCIICase(as, "track")
246#endif
247 || equalLettersIgnoringASCIICase(as, "font"))
248 return as.convertToASCIILowercase();
249 return String();
250}
251
252void HTMLLinkElement::process()
253{
254 if (!isConnected()) {
255 ASSERT(!m_sheet);
256 return;
257 }
258
259 // Prevent recursive loading of link.
260 if (m_isHandlingBeforeLoad)
261 return;
262
263 URL url = getNonEmptyURLAttribute(hrefAttr);
264
265 if (!m_linkLoader.loadLink(m_relAttribute, url, attributeWithoutSynchronization(asAttr), attributeWithoutSynchronization(mediaAttr), attributeWithoutSynchronization(typeAttr), attributeWithoutSynchronization(crossoriginAttr), attributeWithoutSynchronization(imagesrcsetAttr), attributeWithoutSynchronization(imagesizesAttr), document()))
266 return;
267
268 bool treatAsStyleSheet = m_relAttribute.isStyleSheet
269 || (document().settings().treatsAnyTextCSSLinkAsStylesheet() && m_type.containsIgnoringASCIICase("text/css"));
270
271 if (m_disabledState != Disabled && treatAsStyleSheet && document().frame() && url.isValid()) {
272 String charset = attributeWithoutSynchronization(charsetAttr);
273 if (charset.isEmpty())
274 charset = document().charset();
275
276 if (m_cachedSheet) {
277 removePendingSheet();
278 m_cachedSheet->removeClient(*this);
279 m_cachedSheet = nullptr;
280 }
281
282 {
283 SetForScope<bool> change(m_isHandlingBeforeLoad, true);
284 if (!shouldLoadLink())
285 return;
286 }
287
288 m_loading = true;
289
290 bool mediaQueryMatches = true;
291 if (!m_media.isEmpty()) {
292 Optional<RenderStyle> documentStyle;
293 if (document().hasLivingRenderTree())
294 documentStyle = Style::resolveForDocument(document());
295 auto media = MediaQuerySet::create(m_media, MediaQueryParserContext(document()));
296 LOG(MediaQueries, "HTMLLinkElement::process evaluating queries");
297 mediaQueryMatches = MediaQueryEvaluator { document().frame()->view()->mediaType(), document(), documentStyle ? &*documentStyle : nullptr }.evaluate(media.get());
298 }
299
300 // Don't hold up render tree construction and script execution on stylesheets
301 // that are not needed for the rendering at the moment.
302 bool isActive = mediaQueryMatches && !isAlternate();
303 addPendingSheet(isActive ? ActiveSheet : InactiveSheet);
304
305 // Load stylesheets that are not needed for the rendering immediately with low priority.
306 Optional<ResourceLoadPriority> priority;
307 if (!isActive)
308 priority = ResourceLoadPriority::VeryLow;
309
310 if (document().settings().subresourceIntegrityEnabled())
311 m_integrityMetadataForPendingSheetRequest = attributeWithoutSynchronization(HTMLNames::integrityAttr);
312
313 ResourceLoaderOptions options = CachedResourceLoader::defaultCachedResourceOptions();
314 options.sameOriginDataURLFlag = SameOriginDataURLFlag::Set;
315 if (document().contentSecurityPolicy()->allowStyleWithNonce(attributeWithoutSynchronization(HTMLNames::nonceAttr)))
316 options.contentSecurityPolicyImposition = ContentSecurityPolicyImposition::SkipPolicyCheck;
317 options.integrity = m_integrityMetadataForPendingSheetRequest;
318
319 auto request = createPotentialAccessControlRequest(WTFMove(url), document(), crossOrigin(), WTFMove(options));
320 request.setPriority(WTFMove(priority));
321 request.setCharset(WTFMove(charset));
322 request.setInitiator(*this);
323
324 ASSERT_WITH_SECURITY_IMPLICATION(!m_cachedSheet);
325 m_cachedSheet = document().cachedResourceLoader().requestCSSStyleSheet(WTFMove(request)).value_or(nullptr);
326
327 if (m_cachedSheet)
328 m_cachedSheet->addClient(*this);
329 else {
330 // The request may have been denied if (for example) the stylesheet is local and the document is remote.
331 m_loading = false;
332 sheetLoaded();
333 notifyLoadedSheetAndAllCriticalSubresources(false);
334 }
335 } else if (m_sheet) {
336 // we no longer contain a stylesheet, e.g. perhaps rel or type was changed
337 clearSheet();
338 m_styleScope->didChangeActiveStyleSheetCandidates();
339 }
340}
341
342void HTMLLinkElement::clearSheet()
343{
344 ASSERT(m_sheet);
345 ASSERT(m_sheet->ownerNode() == this);
346 m_sheet->clearOwnerNode();
347 m_sheet = nullptr;
348}
349
350Node::InsertedIntoAncestorResult HTMLLinkElement::insertedIntoAncestor(InsertionType insertionType, ContainerNode& parentOfInsertedTree)
351{
352 HTMLElement::insertedIntoAncestor(insertionType, parentOfInsertedTree);
353 if (!insertionType.connectedToDocument)
354 return InsertedIntoAncestorResult::Done;
355
356 m_styleScope = &Style::Scope::forNode(*this);
357 m_styleScope->addStyleSheetCandidateNode(*this, m_createdByParser);
358
359 return InsertedIntoAncestorResult::NeedsPostInsertionCallback;
360}
361
362void HTMLLinkElement::didFinishInsertingNode()
363{
364 process();
365}
366
367void HTMLLinkElement::removedFromAncestor(RemovalType removalType, ContainerNode& oldParentOfRemovedTree)
368{
369 HTMLElement::removedFromAncestor(removalType, oldParentOfRemovedTree);
370 if (!removalType.disconnectedFromDocument)
371 return;
372
373 m_linkLoader.cancelLoad();
374
375 bool wasLoading = styleSheetIsLoading();
376
377 if (m_sheet)
378 clearSheet();
379
380 if (wasLoading)
381 removePendingSheet();
382
383 if (m_styleScope) {
384 m_styleScope->removeStyleSheetCandidateNode(*this);
385 m_styleScope = nullptr;
386 }
387}
388
389void HTMLLinkElement::finishParsingChildren()
390{
391 m_createdByParser = false;
392 HTMLElement::finishParsingChildren();
393}
394
395void HTMLLinkElement::initializeStyleSheet(Ref<StyleSheetContents>&& styleSheet, const CachedCSSStyleSheet& cachedStyleSheet, MediaQueryParserContext context)
396{
397 // FIXME: originClean should be turned to false except if fetch mode is CORS.
398 Optional<bool> originClean;
399 if (cachedStyleSheet.options().mode == FetchOptions::Mode::Cors)
400 originClean = cachedStyleSheet.isCORSSameOrigin();
401
402 m_sheet = CSSStyleSheet::create(WTFMove(styleSheet), *this, originClean);
403 m_sheet->setMediaQueries(MediaQuerySet::create(m_media, context));
404 if (!isInShadowTree())
405 m_sheet->setTitle(title());
406
407 if (!m_sheet->canAccessRules())
408 m_sheet->contents().setAsOpaque();
409}
410
411void HTMLLinkElement::setCSSStyleSheet(const String& href, const URL& baseURL, const String& charset, const CachedCSSStyleSheet* cachedStyleSheet)
412{
413 if (!isConnected()) {
414 ASSERT(!m_sheet);
415 return;
416 }
417 auto frame = makeRefPtr(document().frame());
418 if (!frame)
419 return;
420
421 // Completing the sheet load may cause scripts to execute.
422 Ref<HTMLLinkElement> protectedThis(*this);
423
424 if (!cachedStyleSheet->errorOccurred() && !matchIntegrityMetadata(*cachedStyleSheet, m_integrityMetadataForPendingSheetRequest)) {
425 document().addConsoleMessage(MessageSource::Security, MessageLevel::Error, makeString("Cannot load stylesheet ", cachedStyleSheet->url().stringCenterEllipsizedToLength(), ". Failed integrity metadata check."));
426
427 m_loading = false;
428 sheetLoaded();
429 notifyLoadedSheetAndAllCriticalSubresources(true);
430 return;
431 }
432
433 CSSParserContext parserContext(document(), baseURL, charset);
434 auto cachePolicy = frame->loader().subresourceCachePolicy(baseURL);
435
436 if (auto restoredSheet = const_cast<CachedCSSStyleSheet*>(cachedStyleSheet)->restoreParsedStyleSheet(parserContext, cachePolicy, frame->loader())) {
437 ASSERT(restoredSheet->isCacheable());
438 ASSERT(!restoredSheet->isLoading());
439 initializeStyleSheet(restoredSheet.releaseNonNull(), *cachedStyleSheet, MediaQueryParserContext(document()));
440
441 m_loading = false;
442 sheetLoaded();
443 notifyLoadedSheetAndAllCriticalSubresources(false);
444 return;
445 }
446
447 auto styleSheet = StyleSheetContents::create(href, parserContext);
448 initializeStyleSheet(styleSheet.copyRef(), *cachedStyleSheet, MediaQueryParserContext(document()));
449
450 // FIXME: Set the visibility option based on m_sheet being clean or not.
451 // Best approach might be to set it on the style sheet content itself or its context parser otherwise.
452 styleSheet.get().parseAuthorStyleSheet(cachedStyleSheet, &document().securityOrigin());
453
454 m_loading = false;
455 styleSheet.get().notifyLoadedSheet(cachedStyleSheet);
456 styleSheet.get().checkLoaded();
457
458 if (styleSheet.get().isCacheable())
459 const_cast<CachedCSSStyleSheet*>(cachedStyleSheet)->saveParsedStyleSheet(WTFMove(styleSheet));
460}
461
462bool HTMLLinkElement::styleSheetIsLoading() const
463{
464 if (m_loading)
465 return true;
466 if (!m_sheet)
467 return false;
468 return m_sheet->contents().isLoading();
469}
470
471DOMTokenList& HTMLLinkElement::sizes()
472{
473 if (!m_sizes)
474 m_sizes = std::make_unique<DOMTokenList>(*this, sizesAttr);
475 return *m_sizes;
476}
477
478void HTMLLinkElement::linkLoaded()
479{
480 m_loadedResource = true;
481 linkLoadEventSender().dispatchEventSoon(*this);
482}
483
484void HTMLLinkElement::linkLoadingErrored()
485{
486 linkErrorEventSender().dispatchEventSoon(*this);
487}
488
489bool HTMLLinkElement::sheetLoaded()
490{
491 if (!styleSheetIsLoading()) {
492 removePendingSheet();
493 return true;
494 }
495 return false;
496}
497
498void HTMLLinkElement::dispatchPendingLoadEvents()
499{
500 linkLoadEventSender().dispatchPendingEvents();
501}
502
503void HTMLLinkElement::dispatchPendingEvent(LinkEventSender* eventSender)
504{
505 ASSERT_UNUSED(eventSender, eventSender == &linkLoadEventSender() || eventSender == &linkErrorEventSender());
506 if (m_loadedResource)
507 dispatchEvent(Event::create(eventNames().loadEvent, Event::CanBubble::No, Event::IsCancelable::No));
508 else
509 dispatchEvent(Event::create(eventNames().errorEvent, Event::CanBubble::No, Event::IsCancelable::No));
510}
511
512DOMTokenList& HTMLLinkElement::relList()
513{
514 if (!m_relList)
515 m_relList = std::make_unique<DOMTokenList>(*this, HTMLNames::relAttr, [](Document& document, StringView token) {
516 return LinkRelAttribute::isSupported(document, token);
517 });
518 return *m_relList;
519}
520
521void HTMLLinkElement::notifyLoadedSheetAndAllCriticalSubresources(bool errorOccurred)
522{
523 if (m_firedLoad)
524 return;
525 m_loadedResource = !errorOccurred;
526 linkLoadEventSender().dispatchEventSoon(*this);
527 m_firedLoad = true;
528}
529
530void HTMLLinkElement::startLoadingDynamicSheet()
531{
532 // We don't support multiple active sheets.
533 ASSERT(m_pendingSheetType < ActiveSheet);
534 addPendingSheet(ActiveSheet);
535}
536
537bool HTMLLinkElement::isURLAttribute(const Attribute& attribute) const
538{
539 return attribute.name().localName() == hrefAttr || HTMLElement::isURLAttribute(attribute);
540}
541
542void HTMLLinkElement::defaultEventHandler(Event& event)
543{
544 if (MouseEvent::canTriggerActivationBehavior(event)) {
545 handleClick(event);
546 return;
547 }
548 HTMLElement::defaultEventHandler(event);
549}
550
551void HTMLLinkElement::handleClick(Event& event)
552{
553 event.setDefaultHandled();
554 URL url = href();
555 if (url.isNull())
556 return;
557 RefPtr<Frame> frame = document().frame();
558 if (!frame)
559 return;
560 frame->loader().urlSelected(url, target(), &event, LockHistory::No, LockBackForwardList::No, MaybeSendReferrer, document().shouldOpenExternalURLsPolicyToPropagate());
561}
562
563URL HTMLLinkElement::href() const
564{
565 return document().completeURL(attributeWithoutSynchronization(hrefAttr));
566}
567
568const AtomicString& HTMLLinkElement::rel() const
569{
570 return attributeWithoutSynchronization(relAttr);
571}
572
573String HTMLLinkElement::target() const
574{
575 return attributeWithoutSynchronization(targetAttr);
576}
577
578const AtomicString& HTMLLinkElement::type() const
579{
580 return attributeWithoutSynchronization(typeAttr);
581}
582
583Optional<LinkIconType> HTMLLinkElement::iconType() const
584{
585 return m_relAttribute.iconType;
586}
587
588void HTMLLinkElement::addSubresourceAttributeURLs(ListHashSet<URL>& urls) const
589{
590 HTMLElement::addSubresourceAttributeURLs(urls);
591
592 // Favicons are handled by a special case in LegacyWebArchive::create()
593 if (m_relAttribute.iconType)
594 return;
595
596 if (!m_relAttribute.isStyleSheet)
597 return;
598
599 // Append the URL of this link element.
600 addSubresourceURL(urls, href());
601
602 if (auto styleSheet = makeRefPtr(this->sheet())) {
603 styleSheet->contents().traverseSubresources([&] (auto& resource) {
604 urls.add(resource.url());
605 return false;
606 });
607 }
608}
609
610void HTMLLinkElement::addPendingSheet(PendingSheetType type)
611{
612 if (type <= m_pendingSheetType)
613 return;
614 m_pendingSheetType = type;
615
616 if (m_pendingSheetType == InactiveSheet)
617 return;
618 ASSERT(m_styleScope);
619 m_styleScope->addPendingSheet(*this);
620}
621
622void HTMLLinkElement::removePendingSheet()
623{
624 PendingSheetType type = m_pendingSheetType;
625 m_pendingSheetType = Unknown;
626
627 if (type == Unknown)
628 return;
629
630 ASSERT(m_styleScope);
631 if (type == InactiveSheet) {
632 // Document just needs to know about the sheet for exposure through document.styleSheets
633 m_styleScope->didChangeActiveStyleSheetCandidates();
634 return;
635 }
636
637 m_styleScope->removePendingSheet(*this);
638}
639
640} // namespace WebCore
641