/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "Link.h" #include "mozilla/Components.h" #include "mozilla/EventStates.h" #include "mozilla/MemoryReporting.h" #include "mozilla/dom/Element.h" #if defined(MOZ_PLACES) # include "mozilla/places/History.h" #else # include "mozilla/IHistory.h" #endif #include "nsIURL.h" #include "nsIURIMutator.h" #include "nsISizeOf.h" #include "nsIDocShell.h" #include "nsIPrefetchService.h" #include "nsStyleLinkElement.h" #include "nsEscape.h" #include "nsGkAtoms.h" #include "nsHTMLDNSPrefetch.h" #include "nsString.h" #include "mozAutoDocUpdate.h" #include "mozilla/Services.h" #include "nsAttrValueInlines.h" #include "HTMLLinkElement.h" namespace mozilla { namespace dom { #if defined(MOZ_PLACES) using places::History; #endif Link::Link(Element* aElement) : mElement(aElement), mLinkState(eLinkState_NotLink), mNeedsRegistration(false), mRegistered(false), mHasPendingLinkUpdate(false), mInDNSPrefetch(false), mHistory(true) { MOZ_ASSERT(mElement, "Must have an element"); } Link::Link() : mElement(nullptr), mLinkState(eLinkState_NotLink), mNeedsRegistration(false), mRegistered(false), mHasPendingLinkUpdate(false), mInDNSPrefetch(false), mHistory(false) {} Link::~Link() { // !mElement is for mock_Link. MOZ_ASSERT(!mElement || !mElement->IsInComposedDoc()); if (IsInDNSPrefetch()) { nsHTMLDNSPrefetch::LinkDestroyed(this); } UnregisterFromHistory(); } bool Link::ElementHasHref() const { return mElement->HasAttr(kNameSpaceID_None, nsGkAtoms::href) || (!mElement->IsHTMLElement() && mElement->HasAttr(kNameSpaceID_XLink, nsGkAtoms::href)); } void Link::TryDNSPrefetch() { MOZ_ASSERT(mElement->IsInComposedDoc()); if (ElementHasHref() && nsHTMLDNSPrefetch::IsAllowed(mElement->OwnerDoc())) { nsHTMLDNSPrefetch::PrefetchLow(this); } } void Link::CancelDNSPrefetch(nsWrapperCache::FlagsType aDeferredFlag, nsWrapperCache::FlagsType aRequestedFlag) { // If prefetch was deferred, clear flag and move on if (mElement->HasFlag(aDeferredFlag)) { mElement->UnsetFlags(aDeferredFlag); // Else if prefetch was requested, clear flag and send cancellation } else if (mElement->HasFlag(aRequestedFlag)) { mElement->UnsetFlags(aRequestedFlag); // Possible that hostname could have changed since binding, but since this // covers common cases, most DNS prefetch requests will be canceled nsHTMLDNSPrefetch::CancelPrefetchLow(this, NS_ERROR_ABORT); } } void Link::GetContentPolicyMimeTypeMedia(nsAttrValue& aAsAttr, nsContentPolicyType& aPolicyType, nsString& aMimeType, nsAString& aMedia) { nsAutoString as; mElement->GetAttr(kNameSpaceID_None, nsGkAtoms::as, as); Link::ParseAsValue(as, aAsAttr); aPolicyType = AsValueToContentPolicy(aAsAttr); nsAutoString type; mElement->GetAttr(kNameSpaceID_None, nsGkAtoms::type, type); nsAutoString notUsed; nsContentUtils::SplitMimeType(type, aMimeType, notUsed); mElement->GetAttr(kNameSpaceID_None, nsGkAtoms::media, aMedia); } void Link::TryDNSPrefetchOrPreconnectOrPrefetchOrPreloadOrPrerender() { MOZ_ASSERT(mElement->IsInComposedDoc()); if (!ElementHasHref()) { return; } nsAutoString rel; if (!mElement->GetAttr(kNameSpaceID_None, nsGkAtoms::rel, rel)) { return; } if (!nsContentUtils::PrefetchPreloadEnabled( mElement->OwnerDoc()->GetDocShell())) { return; } uint32_t linkTypes = nsStyleLinkElement::ParseLinkTypes(rel); if ((linkTypes & nsStyleLinkElement::ePREFETCH) || (linkTypes & nsStyleLinkElement::eNEXT) || (linkTypes & nsStyleLinkElement::ePRELOAD)) { nsCOMPtr prefetchService( components::Prefetch::Service()); if (prefetchService) { nsCOMPtr uri(GetURI()); if (uri) { if (linkTypes & nsStyleLinkElement::ePRELOAD) { nsAttrValue asAttr; nsContentPolicyType policyType; nsAutoString mimeType; nsAutoString media; GetContentPolicyMimeTypeMedia(asAttr, policyType, mimeType, media); if (policyType == nsIContentPolicy::TYPE_INVALID) { // Ignore preload with a wrong or empty as attribute. return; } if (!HTMLLinkElement::CheckPreloadAttrs(asAttr, mimeType, media, mElement->OwnerDoc())) { policyType = nsIContentPolicy::TYPE_INVALID; } prefetchService->PreloadURI(uri, mElement->OwnerDoc()->GetDocumentURI(), mElement, policyType); } else { prefetchService->PrefetchURI( uri, mElement->OwnerDoc()->GetDocumentURI(), mElement, linkTypes & nsStyleLinkElement::ePREFETCH); } return; } } } if (linkTypes & nsStyleLinkElement::ePRECONNECT) { nsCOMPtr uri(GetURI()); if (uri && mElement->OwnerDoc()) { mElement->OwnerDoc()->MaybePreconnect( uri, Element::AttrValueToCORSMode( mElement->GetParsedAttr(nsGkAtoms::crossorigin))); return; } } if (linkTypes & nsStyleLinkElement::eDNS_PREFETCH) { if (nsHTMLDNSPrefetch::IsAllowed(mElement->OwnerDoc())) { nsHTMLDNSPrefetch::PrefetchLow(this); } } } void Link::UpdatePreload(nsAtom* aName, const nsAttrValue* aValue, const nsAttrValue* aOldValue) { MOZ_ASSERT(mElement->IsInComposedDoc()); if (!ElementHasHref()) { return; } nsAutoString rel; if (!mElement->GetAttr(kNameSpaceID_None, nsGkAtoms::rel, rel)) { return; } if (!nsContentUtils::PrefetchPreloadEnabled( mElement->OwnerDoc()->GetDocShell())) { return; } uint32_t linkTypes = nsStyleLinkElement::ParseLinkTypes(rel); if (!(linkTypes & nsStyleLinkElement::ePRELOAD)) { return; } nsCOMPtr prefetchService(components::Prefetch::Service()); if (!prefetchService) { return; } nsCOMPtr uri(GetURI()); if (!uri) { return; } nsAttrValue asAttr; nsContentPolicyType asPolicyType; nsAutoString mimeType; nsAutoString media; GetContentPolicyMimeTypeMedia(asAttr, asPolicyType, mimeType, media); if (asPolicyType == nsIContentPolicy::TYPE_INVALID) { // Ignore preload with a wrong or empty as attribute, but be sure to cancel // the old one. prefetchService->CancelPrefetchPreloadURI(uri, mElement); return; } nsContentPolicyType policyType = asPolicyType; if (!HTMLLinkElement::CheckPreloadAttrs(asAttr, mimeType, media, mElement->OwnerDoc())) { policyType = nsIContentPolicy::TYPE_INVALID; } if (aName == nsGkAtoms::crossorigin) { CORSMode corsMode = Element::AttrValueToCORSMode(aValue); CORSMode oldCorsMode = Element::AttrValueToCORSMode(aOldValue); if (corsMode != oldCorsMode) { prefetchService->CancelPrefetchPreloadURI(uri, mElement); prefetchService->PreloadURI(uri, mElement->OwnerDoc()->GetDocumentURI(), mElement, policyType); } return; } nsContentPolicyType oldPolicyType; if (aName == nsGkAtoms::as) { if (aOldValue) { oldPolicyType = AsValueToContentPolicy(*aOldValue); if (!HTMLLinkElement::CheckPreloadAttrs(*aOldValue, mimeType, media, mElement->OwnerDoc())) { oldPolicyType = nsIContentPolicy::TYPE_INVALID; } } else { oldPolicyType = nsIContentPolicy::TYPE_INVALID; } } else if (aName == nsGkAtoms::type) { nsAutoString oldType; nsAutoString notUsed; if (aOldValue) { aOldValue->ToString(oldType); } else { oldType = EmptyString(); } nsAutoString oldMimeType; nsContentUtils::SplitMimeType(oldType, oldMimeType, notUsed); if (HTMLLinkElement::CheckPreloadAttrs(asAttr, oldMimeType, media, mElement->OwnerDoc())) { oldPolicyType = asPolicyType; } else { oldPolicyType = nsIContentPolicy::TYPE_INVALID; } } else { MOZ_ASSERT(aName == nsGkAtoms::media); nsAutoString oldMedia; if (aOldValue) { aOldValue->ToString(oldMedia); } else { oldMedia = EmptyString(); } if (HTMLLinkElement::CheckPreloadAttrs(asAttr, mimeType, oldMedia, mElement->OwnerDoc())) { oldPolicyType = asPolicyType; } else { oldPolicyType = nsIContentPolicy::TYPE_INVALID; } } if ((policyType != oldPolicyType) && (oldPolicyType != nsIContentPolicy::TYPE_INVALID)) { prefetchService->CancelPrefetchPreloadURI(uri, mElement); } // Trigger a new preload if the policy type has changed. // Also trigger load if the new policy type is invalid, this will only // trigger an error event. if ((policyType != oldPolicyType) || (policyType == nsIContentPolicy::TYPE_INVALID)) { prefetchService->PreloadURI(uri, mElement->OwnerDoc()->GetDocumentURI(), mElement, policyType); } } void Link::CancelPrefetchOrPreload() { nsCOMPtr prefetchService(components::Prefetch::Service()); if (prefetchService) { nsCOMPtr uri(GetURI()); if (uri) { prefetchService->CancelPrefetchPreloadURI(uri, mElement); } } } void Link::SetLinkState(nsLinkState aState) { NS_ASSERTION(mRegistered, "Setting the link state of an unregistered Link!"); NS_ASSERTION(mLinkState != aState, "Setting state to the currently set state!"); // Set our current state as appropriate. mLinkState = aState; // Per IHistory interface documentation, we are no longer registered. mRegistered = false; MOZ_ASSERT(LinkState() == NS_EVENT_STATE_VISITED || LinkState() == NS_EVENT_STATE_UNVISITED, "Unexpected state obtained from LinkState()!"); // Tell the element to update its visited state mElement->UpdateState(true); } EventStates Link::LinkState() const { // We are a constant method, but we are just lazily doing things and have to // track that state. Cast away that constness! Link* self = const_cast(this); Element* element = self->mElement; // If we have not yet registered for notifications and need to, // due to our href changing, register now! if (!mRegistered && mNeedsRegistration && element->IsInComposedDoc() && !HasPendingLinkUpdate()) { // Only try and register once. self->mNeedsRegistration = false; nsCOMPtr hrefURI(GetURI()); // Assume that we are not visited until we are told otherwise. self->mLinkState = eLinkState_Unvisited; // Make sure the href attribute has a valid link (bug 23209). // If we have a good href, register with History if available. if (mHistory && hrefURI) { #ifdef ANDROID nsCOMPtr history = services::GetHistoryService(); #elif defined(MOZ_PLACES) History* history = History::GetService(); #else nsCOMPtr history; #endif if (history) { nsresult rv = history->RegisterVisitedCallback(hrefURI, self); if (NS_SUCCEEDED(rv)) { self->mRegistered = true; // And make sure we are in the document's link map. element->GetComposedDoc()->AddStyleRelevantLink(self); } } } } // Otherwise, return our known state. if (mLinkState == eLinkState_Visited) { return NS_EVENT_STATE_VISITED; } if (mLinkState == eLinkState_Unvisited) { return NS_EVENT_STATE_UNVISITED; } return EventStates(); } nsIURI* Link::GetURI() const { // If we have this URI cached, use it. if (mCachedURI) { return mCachedURI; } // Otherwise obtain it. Link* self = const_cast(this); Element* element = self->mElement; mCachedURI = element->GetHrefURI(); return mCachedURI; } void Link::SetProtocol(const nsAString& aProtocol) { nsCOMPtr uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. return; } nsAString::const_iterator start, end; aProtocol.BeginReading(start); aProtocol.EndReading(end); nsAString::const_iterator iter(start); (void)FindCharInReadable(':', iter, end); nsresult rv = NS_MutateURI(uri) .SetScheme(NS_ConvertUTF16toUTF8(Substring(start, iter))) .Finalize(uri); if (NS_FAILED(rv)) { return; } SetHrefAttribute(uri); } void Link::SetPassword(const nsAString& aPassword) { nsCOMPtr uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. return; } nsresult rv = NS_MutateURI(uri) .SetPassword(NS_ConvertUTF16toUTF8(aPassword)) .Finalize(uri); if (NS_SUCCEEDED(rv)) { SetHrefAttribute(uri); } } void Link::SetUsername(const nsAString& aUsername) { nsCOMPtr uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. return; } nsresult rv = NS_MutateURI(uri) .SetUsername(NS_ConvertUTF16toUTF8(aUsername)) .Finalize(uri); if (NS_SUCCEEDED(rv)) { SetHrefAttribute(uri); } } void Link::SetHost(const nsAString& aHost) { nsCOMPtr uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. return; } nsresult rv = NS_MutateURI(uri).SetHostPort(NS_ConvertUTF16toUTF8(aHost)).Finalize(uri); if (NS_FAILED(rv)) { return; } SetHrefAttribute(uri); } void Link::SetHostname(const nsAString& aHostname) { nsCOMPtr uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. return; } nsresult rv = NS_MutateURI(uri).SetHost(NS_ConvertUTF16toUTF8(aHostname)).Finalize(uri); if (NS_FAILED(rv)) { return; } SetHrefAttribute(uri); } void Link::SetPathname(const nsAString& aPathname) { nsCOMPtr uri(GetURI()); nsCOMPtr url(do_QueryInterface(uri)); if (!url) { // Ignore failures to be compatible with NS4. return; } nsresult rv = NS_MutateURI(uri) .SetFilePath(NS_ConvertUTF16toUTF8(aPathname)) .Finalize(uri); if (NS_FAILED(rv)) { return; } SetHrefAttribute(uri); } void Link::SetSearch(const nsAString& aSearch) { nsCOMPtr uri(GetURI()); nsCOMPtr url(do_QueryInterface(uri)); if (!url) { // Ignore failures to be compatible with NS4. return; } auto encoding = mElement->OwnerDoc()->GetDocumentCharacterSet(); nsresult rv = NS_MutateURI(uri) .SetQueryWithEncoding(NS_ConvertUTF16toUTF8(aSearch), encoding) .Finalize(uri); if (NS_FAILED(rv)) { return; } SetHrefAttribute(uri); } void Link::SetPort(const nsAString& aPort) { nsCOMPtr uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. return; } nsresult rv; nsAutoString portStr(aPort); // nsIURI uses -1 as default value. int32_t port = -1; if (!aPort.IsEmpty()) { port = portStr.ToInteger(&rv); if (NS_FAILED(rv)) { return; } } rv = NS_MutateURI(uri).SetPort(port).Finalize(uri); if (NS_FAILED(rv)) { return; } SetHrefAttribute(uri); } void Link::SetHash(const nsAString& aHash) { nsCOMPtr uri(GetURI()); if (!uri) { // Ignore failures to be compatible with NS4. return; } nsresult rv = NS_MutateURI(uri).SetRef(NS_ConvertUTF16toUTF8(aHash)).Finalize(uri); if (NS_FAILED(rv)) { return; } SetHrefAttribute(uri); } void Link::GetOrigin(nsAString& aOrigin) { aOrigin.Truncate(); nsCOMPtr uri(GetURI()); if (!uri) { return; } nsString origin; nsContentUtils::GetUTFOrigin(uri, origin); aOrigin.Assign(origin); } void Link::GetProtocol(nsAString& _protocol) { nsCOMPtr uri(GetURI()); if (!uri) { _protocol.AssignLiteral("http"); } else { nsAutoCString scheme; (void)uri->GetScheme(scheme); CopyASCIItoUTF16(scheme, _protocol); } _protocol.Append(char16_t(':')); } void Link::GetUsername(nsAString& aUsername) { aUsername.Truncate(); nsCOMPtr uri(GetURI()); if (!uri) { return; } nsAutoCString username; uri->GetUsername(username); CopyASCIItoUTF16(username, aUsername); } void Link::GetPassword(nsAString& aPassword) { aPassword.Truncate(); nsCOMPtr uri(GetURI()); if (!uri) { return; } nsAutoCString password; uri->GetPassword(password); CopyASCIItoUTF16(password, aPassword); } void Link::GetHost(nsAString& _host) { _host.Truncate(); nsCOMPtr uri(GetURI()); if (!uri) { // Do not throw! Not having a valid URI should result in an empty string. return; } nsAutoCString hostport; nsresult rv = uri->GetHostPort(hostport); if (NS_SUCCEEDED(rv)) { CopyUTF8toUTF16(hostport, _host); } } void Link::GetHostname(nsAString& _hostname) { _hostname.Truncate(); nsCOMPtr uri(GetURI()); if (!uri) { // Do not throw! Not having a valid URI should result in an empty string. return; } nsContentUtils::GetHostOrIPv6WithBrackets(uri, _hostname); } void Link::GetPathname(nsAString& _pathname) { _pathname.Truncate(); nsCOMPtr uri(GetURI()); nsCOMPtr url(do_QueryInterface(uri)); if (!url) { // Do not throw! Not having a valid URI or URL should result in an empty // string. return; } nsAutoCString file; nsresult rv = url->GetFilePath(file); if (NS_SUCCEEDED(rv)) { CopyUTF8toUTF16(file, _pathname); } } void Link::GetSearch(nsAString& _search) { _search.Truncate(); nsCOMPtr uri(GetURI()); nsCOMPtr url(do_QueryInterface(uri)); if (!url) { // Do not throw! Not having a valid URI or URL should result in an empty // string. return; } nsAutoCString search; nsresult rv = url->GetQuery(search); if (NS_SUCCEEDED(rv) && !search.IsEmpty()) { _search.Assign(u'?'); AppendUTF8toUTF16(search, _search); } } void Link::GetPort(nsAString& _port) { _port.Truncate(); nsCOMPtr uri(GetURI()); if (!uri) { // Do not throw! Not having a valid URI should result in an empty string. return; } int32_t port; nsresult rv = uri->GetPort(&port); // Note that failure to get the port from the URI is not necessarily a bad // thing. Some URIs do not have a port. if (NS_SUCCEEDED(rv) && port != -1) { nsAutoString portStr; portStr.AppendInt(port, 10); _port.Assign(portStr); } } void Link::GetHash(nsAString& _hash) { _hash.Truncate(); nsCOMPtr uri(GetURI()); if (!uri) { // Do not throw! Not having a valid URI should result in an empty // string. return; } nsAutoCString ref; nsresult rv = uri->GetRef(ref); if (NS_SUCCEEDED(rv) && !ref.IsEmpty()) { _hash.Assign(char16_t('#')); AppendUTF8toUTF16(ref, _hash); } } void Link::ResetLinkState(bool aNotify, bool aHasHref) { nsLinkState defaultState; // The default state for links with an href is unvisited. if (aHasHref) { defaultState = eLinkState_Unvisited; } else { defaultState = eLinkState_NotLink; } // If !mNeedsRegstration, then either we've never registered, or we're // currently registered; in either case, we should remove ourself // from the doc and the history. if (!mNeedsRegistration && mLinkState != eLinkState_NotLink) { Document* doc = mElement->GetComposedDoc(); if (doc && (mRegistered || mLinkState == eLinkState_Visited)) { // Tell the document to forget about this link if we've registered // with it before. doc->ForgetLink(this); } } // If we have an href, we should register with the history. mNeedsRegistration = aHasHref; // If we've cached the URI, reset always invalidates it. UnregisterFromHistory(); mCachedURI = nullptr; // Update our state back to the default. mLinkState = defaultState; // We have to be very careful here: if aNotify is false we do NOT // want to call UpdateState, because that will call into LinkState() // and try to start off loads, etc. But ResetLinkState is called // with aNotify false when things are in inconsistent states, so // we'll get confused in that situation. Instead, just silently // update the link state on mElement. Since we might have set the // link state to unvisited, make sure to update with that state if // required. if (aNotify) { mElement->UpdateState(aNotify); } else { if (mLinkState == eLinkState_Unvisited) { mElement->UpdateLinkState(NS_EVENT_STATE_UNVISITED); } else { mElement->UpdateLinkState(EventStates()); } } } void Link::UnregisterFromHistory() { // If we are not registered, we have nothing to do. if (!mRegistered) { return; } // And tell History to stop tracking us. if (mHistory && mCachedURI) { #ifdef ANDROID nsCOMPtr history = services::GetHistoryService(); #elif defined(MOZ_PLACES) History* history = History::GetService(); #else nsCOMPtr history; #endif if (history) { nsresult rv = history->UnregisterVisitedCallback(mCachedURI, this); NS_ASSERTION(NS_SUCCEEDED(rv), "This should only fail if we misuse the API!"); if (NS_SUCCEEDED(rv)) { mRegistered = false; } } } } void Link::SetHrefAttribute(nsIURI* aURI) { NS_ASSERTION(aURI, "Null URI is illegal!"); // if we change this code to not reserialize we need to do something smarter // in SetProtocol because changing the protocol of an URI can change the // "nature" of the nsIURL/nsIURI implementation. nsAutoCString href; (void)aURI->GetSpec(href); (void)mElement->SetAttr(kNameSpaceID_None, nsGkAtoms::href, NS_ConvertUTF8toUTF16(href), true); } size_t Link::SizeOfExcludingThis(mozilla::SizeOfState& aState) const { size_t n = 0; if (mCachedURI) { nsCOMPtr iface = do_QueryInterface(mCachedURI); if (iface) { n += iface->SizeOfIncludingThis(aState.mMallocSizeOf); } } // The following members don't need to be measured: // - mElement, because it is a pointer-to-self used to avoid QIs return n; } static const nsAttrValue::EnumTable kAsAttributeTable[] = { {"", DESTINATION_INVALID}, {"audio", DESTINATION_AUDIO}, {"font", DESTINATION_FONT}, {"image", DESTINATION_IMAGE}, {"script", DESTINATION_SCRIPT}, {"style", DESTINATION_STYLE}, {"track", DESTINATION_TRACK}, {"video", DESTINATION_VIDEO}, {"fetch", DESTINATION_FETCH}, {nullptr, 0}}; /* static */ void Link::ParseAsValue(const nsAString& aValue, nsAttrValue& aResult) { DebugOnly success = aResult.ParseEnumValue(aValue, kAsAttributeTable, false, // default value is a empty string // if aValue is not a value we // understand &kAsAttributeTable[0]); MOZ_ASSERT(success); } /* static */ nsContentPolicyType Link::AsValueToContentPolicy(const nsAttrValue& aValue) { switch (aValue.GetEnumValue()) { case DESTINATION_INVALID: return nsIContentPolicy::TYPE_INVALID; case DESTINATION_AUDIO: return nsIContentPolicy::TYPE_INTERNAL_AUDIO; case DESTINATION_TRACK: return nsIContentPolicy::TYPE_INTERNAL_TRACK; case DESTINATION_VIDEO: return nsIContentPolicy::TYPE_INTERNAL_VIDEO; case DESTINATION_FONT: return nsIContentPolicy::TYPE_FONT; case DESTINATION_IMAGE: return nsIContentPolicy::TYPE_IMAGE; case DESTINATION_SCRIPT: return nsIContentPolicy::TYPE_SCRIPT; case DESTINATION_STYLE: return nsIContentPolicy::TYPE_STYLESHEET; case DESTINATION_FETCH: return nsIContentPolicy::TYPE_OTHER; } return nsIContentPolicy::TYPE_INVALID; } } // namespace dom } // namespace mozilla