dom/base/Link.cpp
author Matt Woodrow <mwoodrow@mozilla.com>
Wed, 20 Nov 2019 03:29:25 +0000
changeset 503066 e76dbab2aea8354660281221c1aa08356107881c
parent 500412 89fad029456188f03a670ef5f08a5d0856a728b1
permissions -rw-r--r--
Bug 1597015 - Split DocumentChannelParent into two separate classes (DocumentLoadListener and DocumentChannelParent). r=mayhemer,jya Currently DocumentChannelParent serves two fairly separate functions. It acts as a logic controller for a connecting load (ultimately on behalf of a CanonicalBrowsingContext), that creates a channel and once it gets a response, figures out the right thing to do with it (send to the originating process, send to a new process, handle as a download etc). It's the parent-side IPDL actor for an nsIChannel implementation that we hold in the docshell as a placeholder while the above happens. This is largely a backwards compatibility hack since docshell expects to have a channel when a load has been initiated (but in the future we could do loads more directly through BrowsingContext and rewrite docshell to understand waiting on that). This patch splits this into two separate classes (and adds more documentation explaining exactly what each one does). It shouldn't affect any behaviour. Differential Revision: https://phabricator.services.mozilla.com/D53383

/* -*- 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/EventStates.h"
#include "mozilla/MemoryReporting.h"
#include "mozilla/dom/Element.h"
#include "mozilla/IHistory.h"
#include "mozilla/StaticPrefs_layout.h"
#include "nsLayoutUtils.h"
#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 {

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::VisitedQueryFinished(bool aVisited) {
  MOZ_ASSERT(mRegistered, "Setting the link state of an unregistered Link!");
  MOZ_ASSERT(mLinkState == eLinkState_Unvisited,
             "Why would we want to know our visited state otherwise?");

  auto newState = aVisited ? eLinkState_Visited : eLinkState_Unvisited;

  // Set our current state as appropriate.
  mLinkState = newState;

  // We will be no longer registered if we're visited, as it'd be pointless, we
  // never transition from visited -> unvisited.
  if (aVisited) {
    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);

  if (StaticPrefs::layout_css_always_repaint_on_unvisited()) {
    // Even if the state didn't actually change, we need to repaint in order for
    // the visited state not to be observable.
    nsLayoutUtils::PostRestyleEvent(GetElement(), RestyleHint::RestyleSubtree(),
                                    nsChangeHint_RepaintFrame);
  }
}

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!
  //
  // XXX(emilio): that's evil.
  Link* self = const_cast<Link*>(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<nsIURI> 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) {
      if (nsCOMPtr<IHistory> history = services::GetHistoryService()) {
        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<Link*>(this);
  Element* element = self->mElement;
  mCachedURI = element->GetHrefURI();

  return mCachedURI;
}

void Link::SetProtocol(const nsAString& aProtocol) {
  nsCOMPtr<nsIURI> 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<nsIURI> 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<nsIURI> 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<nsIURI> 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<nsIURI> 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<nsIURI> uri(GetURI());
  nsCOMPtr<nsIURL> 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<nsIURI> uri(GetURI());
  nsCOMPtr<nsIURL> 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<nsIURI> 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<nsIURI> 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<nsIURI> uri(GetURI());
  if (!uri) {
    return;
  }

  nsString origin;
  nsContentUtils::GetUTFOrigin(uri, origin);
  aOrigin.Assign(origin);
}

void Link::GetProtocol(nsAString& _protocol) {
  nsCOMPtr<nsIURI> 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<nsIURI> uri(GetURI());
  if (!uri) {
    return;
  }

  nsAutoCString username;
  uri->GetUsername(username);
  CopyASCIItoUTF16(username, aUsername);
}

void Link::GetPassword(nsAString& aPassword) {
  aPassword.Truncate();

  nsCOMPtr<nsIURI> uri(GetURI());
  if (!uri) {
    return;
  }

  nsAutoCString password;
  uri->GetPassword(password);
  CopyASCIItoUTF16(password, aPassword);
}

void Link::GetHost(nsAString& _host) {
  _host.Truncate();

  nsCOMPtr<nsIURI> 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<nsIURI> 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<nsIURI> uri(GetURI());
  nsCOMPtr<nsIURL> 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<nsIURI> uri(GetURI());
  nsCOMPtr<nsIURL> 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<nsIURI> 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<nsIURI> 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, and we're not a <link>, we should register with the
  // history.
  //
  // FIXME(emilio): Do we really want to allow all MathML elements to be
  // :visited? That seems not great.
  mNeedsRegistration = aHasHref && !mElement->IsHTMLElement(nsGkAtoms::link);

  // 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) {
    if (nsCOMPtr<IHistory> history = services::GetHistoryService()) {
      history->UnregisterVisitedCallback(mCachedURI, this);
      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<nsISizeOf> 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;
}

}  // namespace dom
}  // namespace mozilla