/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: sw=2 ts=2 et : * 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/dom/Element.h" #include "nsEventStates.h" #include "nsIURL.h" #include "nsISizeOf.h" #include "nsEscape.h" #include "nsGkAtoms.h" #include "nsString.h" #include "mozAutoDocUpdate.h" #include "mozilla/Services.h" namespace mozilla { namespace dom { Link::Link(Element *aElement) : mElement(aElement) , mHistory(services::GetHistoryService()) , mLinkState(eLinkState_NotLink) , mNeedsRegistration(false) , mRegistered(false) { NS_ABORT_IF_FALSE(mElement, "Must have an element"); } Link::~Link() { UnregisterFromHistory(); } bool Link::ElementHasHref() const { return ((!mElement->IsSVG() && mElement->HasAttr(kNameSpaceID_None, nsGkAtoms::href)) || (!mElement->IsHTML() && mElement->HasAttr(kNameSpaceID_XLink, nsGkAtoms::href))); } nsLinkState Link::GetLinkState() const { NS_ASSERTION(mRegistered, "Getting the link state of an unregistered Link!"); return nsLinkState(mLinkState); } 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; NS_ABORT_IF_FALSE(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); } nsEventStates 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->IsInDoc()) { // 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) { nsresult rv = mHistory->RegisterVisitedCallback(hrefURI, self); if (NS_SUCCEEDED(rv)) { self->mRegistered = true; // And make sure we are in the document's link map. element->GetCurrentDoc()->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 nsEventStates(); } already_AddRefed Link::GetURI() const { nsCOMPtr uri(mCachedURI); // If we have this URI cached, use it. if (uri) { return uri.forget(); } // Otherwise obtain it. Link *self = const_cast(this); Element *element = self->mElement; uri = element->GetHrefURI(); // We want to cache the URI if we have it if (uri) { mCachedURI = uri; } return uri.forget(); } nsresult Link::SetProtocol(const nsAString &aProtocol) { nsCOMPtr uri(GetURIToMutate()); if (!uri) { // Ignore failures to be compatible with NS4. return NS_OK; } nsAString::const_iterator start, end; aProtocol.BeginReading(start); aProtocol.EndReading(end); nsAString::const_iterator iter(start); (void)FindCharInReadable(':', iter, end); (void)uri->SetScheme(NS_ConvertUTF16toUTF8(Substring(start, iter))); SetHrefAttribute(uri); return NS_OK; } nsresult Link::SetHost(const nsAString &aHost) { nsCOMPtr uri(GetURIToMutate()); if (!uri) { // Ignore failures to be compatible with NS4. return NS_OK; } // We cannot simply call nsIURI::SetHost because that would treat the name as // an IPv6 address (like http:://[server:443]/). We also cannot call // nsIURI::SetHostPort because that isn't implemented. Sadfaces. // First set the hostname. nsAString::const_iterator start, end; aHost.BeginReading(start); aHost.EndReading(end); nsAString::const_iterator iter(start); (void)FindCharInReadable(':', iter, end); NS_ConvertUTF16toUTF8 host(Substring(start, iter)); (void)uri->SetHost(host); // Also set the port if needed. if (iter != end) { iter++; if (iter != end) { nsAutoString portStr(Substring(iter, end)); nsresult rv; int32_t port = portStr.ToInteger(&rv); if (NS_SUCCEEDED(rv)) { (void)uri->SetPort(port); } } }; SetHrefAttribute(uri); return NS_OK; } nsresult Link::SetHostname(const nsAString &aHostname) { nsCOMPtr uri(GetURIToMutate()); if (!uri) { // Ignore failures to be compatible with NS4. return NS_OK; } (void)uri->SetHost(NS_ConvertUTF16toUTF8(aHostname)); SetHrefAttribute(uri); return NS_OK; } nsresult Link::SetPathname(const nsAString &aPathname) { nsCOMPtr uri(GetURIToMutate()); nsCOMPtr url(do_QueryInterface(uri)); if (!url) { // Ignore failures to be compatible with NS4. return NS_OK; } (void)url->SetFilePath(NS_ConvertUTF16toUTF8(aPathname)); SetHrefAttribute(uri); return NS_OK; } nsresult Link::SetSearch(const nsAString &aSearch) { nsCOMPtr uri(GetURIToMutate()); nsCOMPtr url(do_QueryInterface(uri)); if (!url) { // Ignore failures to be compatible with NS4. return NS_OK; } (void)url->SetQuery(NS_ConvertUTF16toUTF8(aSearch)); SetHrefAttribute(uri); return NS_OK; } nsresult Link::SetPort(const nsAString &aPort) { nsCOMPtr uri(GetURIToMutate()); if (!uri) { // Ignore failures to be compatible with NS4. return NS_OK; } nsresult rv; nsAutoString portStr(aPort); int32_t port = portStr.ToInteger(&rv); if (NS_FAILED(rv)) { return NS_OK; } (void)uri->SetPort(port); SetHrefAttribute(uri); return NS_OK; } nsresult Link::SetHash(const nsAString &aHash) { nsCOMPtr uri(GetURIToMutate()); if (!uri) { // Ignore failures to be compatible with NS4. return NS_OK; } (void)uri->SetRef(NS_ConvertUTF16toUTF8(aHash)); SetHrefAttribute(uri); return NS_OK; } nsresult Link::GetProtocol(nsAString &_protocol) { nsCOMPtr uri(GetURI()); if (!uri) { _protocol.AssignLiteral("http"); } else { nsAutoCString scheme; (void)uri->GetScheme(scheme); CopyASCIItoUTF16(scheme, _protocol); } _protocol.Append(PRUnichar(':')); return NS_OK; } nsresult 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 NS_OK; } nsAutoCString hostport; nsresult rv = uri->GetHostPort(hostport); if (NS_SUCCEEDED(rv)) { CopyUTF8toUTF16(hostport, _host); } return NS_OK; } nsresult 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 NS_OK; } nsAutoCString host; nsresult rv = uri->GetHost(host); // Note that failure to get the host from the URI is not necessarily a bad // thing. Some URIs do not have a host. if (NS_SUCCEEDED(rv)) { CopyUTF8toUTF16(host, _hostname); } return NS_OK; } nsresult 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 NS_OK; } nsAutoCString file; nsresult rv = url->GetFilePath(file); NS_ENSURE_SUCCESS(rv, rv); CopyUTF8toUTF16(file, _pathname); return NS_OK; } nsresult 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 NS_OK; } nsAutoCString search; nsresult rv = url->GetQuery(search); if (NS_SUCCEEDED(rv) && !search.IsEmpty()) { CopyUTF8toUTF16(NS_LITERAL_CSTRING("?") + search, _search); } return NS_OK; } nsresult 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 NS_OK; } 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); } return NS_OK; } nsresult 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 NS_OK; } nsAutoCString ref; nsresult rv = uri->GetRef(ref); if (NS_SUCCEEDED(rv) && !ref.IsEmpty()) { NS_UnescapeURL(ref); // XXX may result in random non-ASCII bytes! _hash.Assign(PRUnichar('#')); AppendUTF8toUTF16(ref, _hash); } return NS_OK; } 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) { nsIDocument *doc = mElement->GetCurrentDoc(); if (doc && (mRegistered || mLinkState == eLinkState_Visited)) { // Tell the document to forget about this link if we've registered // with it before. doc->ForgetLink(this); } UnregisterFromHistory(); } // If we have an href, we should register with the history. mNeedsRegistration = aHasHref; // If we've cached the URI, reset always invalidates it. 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(nsEventStates()); } } } void Link::UnregisterFromHistory() { // If we are not registered, we have nothing to do. if (!mRegistered) { return; } NS_ASSERTION(mCachedURI, "mRegistered is true, but we have no cached URI?!"); // And tell History to stop tracking us. if (mHistory) { nsresult rv = mHistory->UnregisterVisitedCallback(mCachedURI, this); NS_ASSERTION(NS_SUCCEEDED(rv), "This should only fail if we misuse the API!"); if (NS_SUCCEEDED(rv)) { mRegistered = false; } } } already_AddRefed Link::GetURIToMutate() { nsCOMPtr uri(GetURI()); if (!uri) { return nullptr; } nsCOMPtr clone; (void)uri->Clone(getter_AddRefs(clone)); return clone.forget(); } void Link::SetHrefAttribute(nsIURI *aURI) { NS_ASSERTION(aURI, "Null URI is illegal!"); nsAutoCString href; (void)aURI->GetSpec(href); (void)mElement->SetAttr(kNameSpaceID_None, nsGkAtoms::href, NS_ConvertUTF8toUTF16(href), true); } size_t Link::SizeOfExcludingThis(nsMallocSizeOfFun aMallocSizeOf) const { size_t n = 0; if (mCachedURI) { nsCOMPtr iface = do_QueryInterface(mCachedURI); if (iface) { n += iface->SizeOfIncludingThis(aMallocSizeOf); } } // The following members don't need to be measured: // - mElement, because it is a pointer-to-self used to avoid QIs // - mHistory, because it is non-owning return n; } } // namespace dom } // namespace mozilla