1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "Link.h"
8 
9 #include "mozilla/EventStates.h"
10 #include "mozilla/MemoryReporting.h"
11 #include "mozilla/dom/Element.h"
12 #include "mozilla/IHistory.h"
13 #include "mozilla/StaticPrefs_layout.h"
14 #include "nsLayoutUtils.h"
15 #include "nsIURL.h"
16 #include "nsIURIMutator.h"
17 #include "nsISizeOf.h"
18 
19 #include "nsEscape.h"
20 #include "nsGkAtoms.h"
21 #include "nsHTMLDNSPrefetch.h"
22 #include "nsString.h"
23 #include "mozAutoDocUpdate.h"
24 
25 #include "mozilla/Services.h"
26 #include "nsAttrValueInlines.h"
27 #include "HTMLLinkElement.h"
28 
29 namespace mozilla {
30 namespace dom {
31 
Link(Element * aElement)32 Link::Link(Element* aElement)
33     : mElement(aElement),
34       mLinkState(eLinkState_NotLink),
35       mNeedsRegistration(false),
36       mRegistered(false),
37       mHasPendingLinkUpdate(false),
38       mInDNSPrefetch(false),
39       mHistory(true) {
40   MOZ_ASSERT(mElement, "Must have an element");
41 }
42 
Link()43 Link::Link()
44     : mElement(nullptr),
45       mLinkState(eLinkState_NotLink),
46       mNeedsRegistration(false),
47       mRegistered(false),
48       mHasPendingLinkUpdate(false),
49       mInDNSPrefetch(false),
50       mHistory(false) {}
51 
~Link()52 Link::~Link() {
53   // !mElement is for mock_Link.
54   MOZ_ASSERT(!mElement || !mElement->IsInComposedDoc());
55   if (IsInDNSPrefetch()) {
56     nsHTMLDNSPrefetch::LinkDestroyed(this);
57   }
58   UnregisterFromHistory();
59 }
60 
ElementHasHref() const61 bool Link::ElementHasHref() const {
62   return mElement->HasAttr(kNameSpaceID_None, nsGkAtoms::href) ||
63          (!mElement->IsHTMLElement() &&
64           mElement->HasAttr(kNameSpaceID_XLink, nsGkAtoms::href));
65 }
66 
TryDNSPrefetch()67 void Link::TryDNSPrefetch() {
68   MOZ_ASSERT(mElement->IsInComposedDoc());
69   if (ElementHasHref() && nsHTMLDNSPrefetch::IsAllowed(mElement->OwnerDoc())) {
70     nsHTMLDNSPrefetch::PrefetchLow(this);
71   }
72 }
73 
CancelDNSPrefetch(nsWrapperCache::FlagsType aDeferredFlag,nsWrapperCache::FlagsType aRequestedFlag)74 void Link::CancelDNSPrefetch(nsWrapperCache::FlagsType aDeferredFlag,
75                              nsWrapperCache::FlagsType aRequestedFlag) {
76   // If prefetch was deferred, clear flag and move on
77   if (mElement->HasFlag(aDeferredFlag)) {
78     mElement->UnsetFlags(aDeferredFlag);
79     // Else if prefetch was requested, clear flag and send cancellation
80   } else if (mElement->HasFlag(aRequestedFlag)) {
81     mElement->UnsetFlags(aRequestedFlag);
82     // Possible that hostname could have changed since binding, but since this
83     // covers common cases, most DNS prefetch requests will be canceled
84     nsHTMLDNSPrefetch::CancelPrefetchLow(this, NS_ERROR_ABORT);
85   }
86 }
87 
VisitedQueryFinished(bool aVisited)88 void Link::VisitedQueryFinished(bool aVisited) {
89   MOZ_ASSERT(mRegistered, "Setting the link state of an unregistered Link!");
90   MOZ_ASSERT(mLinkState == eLinkState_Unvisited,
91              "Why would we want to know our visited state otherwise?");
92 
93   auto newState = aVisited ? eLinkState_Visited : eLinkState_Unvisited;
94 
95   // Set our current state as appropriate.
96   mLinkState = newState;
97 
98   // We will be no longer registered if we're visited, as it'd be pointless, we
99   // never transition from visited -> unvisited.
100   if (aVisited) {
101     mRegistered = false;
102   }
103 
104   MOZ_ASSERT(LinkState() == NS_EVENT_STATE_VISITED ||
105                  LinkState() == NS_EVENT_STATE_UNVISITED,
106              "Unexpected state obtained from LinkState()!");
107 
108   // Tell the element to update its visited state
109   mElement->UpdateState(true);
110 
111   if (StaticPrefs::layout_css_always_repaint_on_unvisited()) {
112     // Even if the state didn't actually change, we need to repaint in order for
113     // the visited state not to be observable.
114     nsLayoutUtils::PostRestyleEvent(GetElement(), RestyleHint::RestyleSubtree(),
115                                     nsChangeHint_RepaintFrame);
116   }
117 }
118 
LinkState() const119 EventStates Link::LinkState() const {
120   // We are a constant method, but we are just lazily doing things and have to
121   // track that state.  Cast away that constness!
122   //
123   // XXX(emilio): that's evil.
124   Link* self = const_cast<Link*>(this);
125 
126   Element* element = self->mElement;
127 
128   // If we have not yet registered for notifications and need to,
129   // due to our href changing, register now!
130   if (!mRegistered && mNeedsRegistration && element->IsInComposedDoc() &&
131       !HasPendingLinkUpdate()) {
132     // Only try and register once.
133     self->mNeedsRegistration = false;
134 
135     nsCOMPtr<nsIURI> hrefURI(GetURI());
136 
137     // Assume that we are not visited until we are told otherwise.
138     self->mLinkState = eLinkState_Unvisited;
139 
140     // Make sure the href attribute has a valid link (bug 23209).
141     // If we have a good href, register with History if available.
142     if (mHistory && hrefURI) {
143       if (nsCOMPtr<IHistory> history = services::GetHistoryService()) {
144         self->mRegistered = true;
145         history->RegisterVisitedCallback(hrefURI, self);
146         // And make sure we are in the document's link map.
147         element->GetComposedDoc()->AddStyleRelevantLink(self);
148       }
149     }
150   }
151 
152   // Otherwise, return our known state.
153   if (mLinkState == eLinkState_Visited) {
154     return NS_EVENT_STATE_VISITED;
155   }
156 
157   if (mLinkState == eLinkState_Unvisited) {
158     return NS_EVENT_STATE_UNVISITED;
159   }
160 
161   return EventStates();
162 }
163 
GetURI() const164 nsIURI* Link::GetURI() const {
165   // If we have this URI cached, use it.
166   if (mCachedURI) {
167     return mCachedURI;
168   }
169 
170   // Otherwise obtain it.
171   Link* self = const_cast<Link*>(this);
172   Element* element = self->mElement;
173   mCachedURI = element->GetHrefURI();
174 
175   return mCachedURI;
176 }
177 
SetProtocol(const nsAString & aProtocol)178 void Link::SetProtocol(const nsAString& aProtocol) {
179   nsCOMPtr<nsIURI> uri(GetURI());
180   if (!uri) {
181     // Ignore failures to be compatible with NS4.
182     return;
183   }
184 
185   nsAString::const_iterator start, end;
186   aProtocol.BeginReading(start);
187   aProtocol.EndReading(end);
188   nsAString::const_iterator iter(start);
189   (void)FindCharInReadable(':', iter, end);
190   nsresult rv = NS_MutateURI(uri)
191                     .SetScheme(NS_ConvertUTF16toUTF8(Substring(start, iter)))
192                     .Finalize(uri);
193   if (NS_FAILED(rv)) {
194     return;
195   }
196 
197   SetHrefAttribute(uri);
198 }
199 
SetPassword(const nsAString & aPassword)200 void Link::SetPassword(const nsAString& aPassword) {
201   nsCOMPtr<nsIURI> uri(GetURI());
202   if (!uri) {
203     // Ignore failures to be compatible with NS4.
204     return;
205   }
206 
207   nsresult rv = NS_MutateURI(uri)
208                     .SetPassword(NS_ConvertUTF16toUTF8(aPassword))
209                     .Finalize(uri);
210   if (NS_SUCCEEDED(rv)) {
211     SetHrefAttribute(uri);
212   }
213 }
214 
SetUsername(const nsAString & aUsername)215 void Link::SetUsername(const nsAString& aUsername) {
216   nsCOMPtr<nsIURI> uri(GetURI());
217   if (!uri) {
218     // Ignore failures to be compatible with NS4.
219     return;
220   }
221 
222   nsresult rv = NS_MutateURI(uri)
223                     .SetUsername(NS_ConvertUTF16toUTF8(aUsername))
224                     .Finalize(uri);
225   if (NS_SUCCEEDED(rv)) {
226     SetHrefAttribute(uri);
227   }
228 }
229 
SetHost(const nsAString & aHost)230 void Link::SetHost(const nsAString& aHost) {
231   nsCOMPtr<nsIURI> uri(GetURI());
232   if (!uri) {
233     // Ignore failures to be compatible with NS4.
234     return;
235   }
236 
237   nsresult rv =
238       NS_MutateURI(uri).SetHostPort(NS_ConvertUTF16toUTF8(aHost)).Finalize(uri);
239   if (NS_FAILED(rv)) {
240     return;
241   }
242   SetHrefAttribute(uri);
243 }
244 
SetHostname(const nsAString & aHostname)245 void Link::SetHostname(const nsAString& aHostname) {
246   nsCOMPtr<nsIURI> uri(GetURI());
247   if (!uri) {
248     // Ignore failures to be compatible with NS4.
249     return;
250   }
251 
252   nsresult rv =
253       NS_MutateURI(uri).SetHost(NS_ConvertUTF16toUTF8(aHostname)).Finalize(uri);
254   if (NS_FAILED(rv)) {
255     return;
256   }
257   SetHrefAttribute(uri);
258 }
259 
SetPathname(const nsAString & aPathname)260 void Link::SetPathname(const nsAString& aPathname) {
261   nsCOMPtr<nsIURI> uri(GetURI());
262   nsCOMPtr<nsIURL> url(do_QueryInterface(uri));
263   if (!url) {
264     // Ignore failures to be compatible with NS4.
265     return;
266   }
267 
268   nsresult rv = NS_MutateURI(uri)
269                     .SetFilePath(NS_ConvertUTF16toUTF8(aPathname))
270                     .Finalize(uri);
271   if (NS_FAILED(rv)) {
272     return;
273   }
274   SetHrefAttribute(uri);
275 }
276 
SetSearch(const nsAString & aSearch)277 void Link::SetSearch(const nsAString& aSearch) {
278   nsCOMPtr<nsIURI> uri(GetURI());
279   nsCOMPtr<nsIURL> url(do_QueryInterface(uri));
280   if (!url) {
281     // Ignore failures to be compatible with NS4.
282     return;
283   }
284 
285   auto encoding = mElement->OwnerDoc()->GetDocumentCharacterSet();
286   nsresult rv =
287       NS_MutateURI(uri)
288           .SetQueryWithEncoding(NS_ConvertUTF16toUTF8(aSearch), encoding)
289           .Finalize(uri);
290   if (NS_FAILED(rv)) {
291     return;
292   }
293   SetHrefAttribute(uri);
294 }
295 
SetPort(const nsAString & aPort)296 void Link::SetPort(const nsAString& aPort) {
297   nsCOMPtr<nsIURI> uri(GetURI());
298   if (!uri) {
299     // Ignore failures to be compatible with NS4.
300     return;
301   }
302 
303   nsresult rv;
304   nsAutoString portStr(aPort);
305 
306   // nsIURI uses -1 as default value.
307   int32_t port = -1;
308   if (!aPort.IsEmpty()) {
309     port = portStr.ToInteger(&rv);
310     if (NS_FAILED(rv)) {
311       return;
312     }
313   }
314 
315   rv = NS_MutateURI(uri).SetPort(port).Finalize(uri);
316   if (NS_FAILED(rv)) {
317     return;
318   }
319   SetHrefAttribute(uri);
320 }
321 
SetHash(const nsAString & aHash)322 void Link::SetHash(const nsAString& aHash) {
323   nsCOMPtr<nsIURI> uri(GetURI());
324   if (!uri) {
325     // Ignore failures to be compatible with NS4.
326     return;
327   }
328 
329   nsresult rv =
330       NS_MutateURI(uri).SetRef(NS_ConvertUTF16toUTF8(aHash)).Finalize(uri);
331   if (NS_FAILED(rv)) {
332     return;
333   }
334 
335   SetHrefAttribute(uri);
336 }
337 
GetOrigin(nsAString & aOrigin)338 void Link::GetOrigin(nsAString& aOrigin) {
339   aOrigin.Truncate();
340 
341   nsCOMPtr<nsIURI> uri(GetURI());
342   if (!uri) {
343     return;
344   }
345 
346   nsString origin;
347   nsContentUtils::GetUTFOrigin(uri, origin);
348   aOrigin.Assign(origin);
349 }
350 
GetProtocol(nsAString & _protocol)351 void Link::GetProtocol(nsAString& _protocol) {
352   nsCOMPtr<nsIURI> uri(GetURI());
353   if (!uri) {
354     _protocol.AssignLiteral("http");
355   } else {
356     nsAutoCString scheme;
357     (void)uri->GetScheme(scheme);
358     CopyASCIItoUTF16(scheme, _protocol);
359   }
360   _protocol.Append(char16_t(':'));
361 }
362 
GetUsername(nsAString & aUsername)363 void Link::GetUsername(nsAString& aUsername) {
364   aUsername.Truncate();
365 
366   nsCOMPtr<nsIURI> uri(GetURI());
367   if (!uri) {
368     return;
369   }
370 
371   nsAutoCString username;
372   uri->GetUsername(username);
373   CopyASCIItoUTF16(username, aUsername);
374 }
375 
GetPassword(nsAString & aPassword)376 void Link::GetPassword(nsAString& aPassword) {
377   aPassword.Truncate();
378 
379   nsCOMPtr<nsIURI> uri(GetURI());
380   if (!uri) {
381     return;
382   }
383 
384   nsAutoCString password;
385   uri->GetPassword(password);
386   CopyASCIItoUTF16(password, aPassword);
387 }
388 
GetHost(nsAString & _host)389 void Link::GetHost(nsAString& _host) {
390   _host.Truncate();
391 
392   nsCOMPtr<nsIURI> uri(GetURI());
393   if (!uri) {
394     // Do not throw!  Not having a valid URI should result in an empty string.
395     return;
396   }
397 
398   nsAutoCString hostport;
399   nsresult rv = uri->GetHostPort(hostport);
400   if (NS_SUCCEEDED(rv)) {
401     CopyUTF8toUTF16(hostport, _host);
402   }
403 }
404 
GetHostname(nsAString & _hostname)405 void Link::GetHostname(nsAString& _hostname) {
406   _hostname.Truncate();
407 
408   nsCOMPtr<nsIURI> uri(GetURI());
409   if (!uri) {
410     // Do not throw!  Not having a valid URI should result in an empty string.
411     return;
412   }
413 
414   nsContentUtils::GetHostOrIPv6WithBrackets(uri, _hostname);
415 }
416 
GetPathname(nsAString & _pathname)417 void Link::GetPathname(nsAString& _pathname) {
418   _pathname.Truncate();
419 
420   nsCOMPtr<nsIURI> uri(GetURI());
421   nsCOMPtr<nsIURL> url(do_QueryInterface(uri));
422   if (!url) {
423     // Do not throw!  Not having a valid URI or URL should result in an empty
424     // string.
425     return;
426   }
427 
428   nsAutoCString file;
429   nsresult rv = url->GetFilePath(file);
430   if (NS_SUCCEEDED(rv)) {
431     CopyUTF8toUTF16(file, _pathname);
432   }
433 }
434 
GetSearch(nsAString & _search)435 void Link::GetSearch(nsAString& _search) {
436   _search.Truncate();
437 
438   nsCOMPtr<nsIURI> uri(GetURI());
439   nsCOMPtr<nsIURL> url(do_QueryInterface(uri));
440   if (!url) {
441     // Do not throw!  Not having a valid URI or URL should result in an empty
442     // string.
443     return;
444   }
445 
446   nsAutoCString search;
447   nsresult rv = url->GetQuery(search);
448   if (NS_SUCCEEDED(rv) && !search.IsEmpty()) {
449     _search.Assign(u'?');
450     AppendUTF8toUTF16(search, _search);
451   }
452 }
453 
GetPort(nsAString & _port)454 void Link::GetPort(nsAString& _port) {
455   _port.Truncate();
456 
457   nsCOMPtr<nsIURI> uri(GetURI());
458   if (!uri) {
459     // Do not throw!  Not having a valid URI should result in an empty string.
460     return;
461   }
462 
463   int32_t port;
464   nsresult rv = uri->GetPort(&port);
465   // Note that failure to get the port from the URI is not necessarily a bad
466   // thing.  Some URIs do not have a port.
467   if (NS_SUCCEEDED(rv) && port != -1) {
468     nsAutoString portStr;
469     portStr.AppendInt(port, 10);
470     _port.Assign(portStr);
471   }
472 }
473 
GetHash(nsAString & _hash)474 void Link::GetHash(nsAString& _hash) {
475   _hash.Truncate();
476 
477   nsCOMPtr<nsIURI> uri(GetURI());
478   if (!uri) {
479     // Do not throw!  Not having a valid URI should result in an empty
480     // string.
481     return;
482   }
483 
484   nsAutoCString ref;
485   nsresult rv = uri->GetRef(ref);
486   if (NS_SUCCEEDED(rv) && !ref.IsEmpty()) {
487     _hash.Assign(char16_t('#'));
488     AppendUTF8toUTF16(ref, _hash);
489   }
490 }
491 
ResetLinkState(bool aNotify,bool aHasHref)492 void Link::ResetLinkState(bool aNotify, bool aHasHref) {
493   nsLinkState defaultState;
494 
495   // The default state for links with an href is unvisited.
496   if (aHasHref) {
497     defaultState = eLinkState_Unvisited;
498   } else {
499     defaultState = eLinkState_NotLink;
500   }
501 
502   // If !mNeedsRegstration, then either we've never registered, or we're
503   // currently registered; in either case, we should remove ourself
504   // from the doc and the history.
505   if (!mNeedsRegistration && mLinkState != eLinkState_NotLink) {
506     Document* doc = mElement->GetComposedDoc();
507     if (doc && (mRegistered || mLinkState == eLinkState_Visited)) {
508       // Tell the document to forget about this link if we've registered
509       // with it before.
510       doc->ForgetLink(this);
511     }
512   }
513 
514   // If we have an href, and we're not a <link>, we should register with the
515   // history.
516   //
517   // FIXME(emilio): Do we really want to allow all MathML elements to be
518   // :visited? That seems not great.
519   mNeedsRegistration = aHasHref && !mElement->IsHTMLElement(nsGkAtoms::link);
520 
521   // If we've cached the URI, reset always invalidates it.
522   UnregisterFromHistory();
523   mCachedURI = nullptr;
524 
525   // Update our state back to the default.
526   mLinkState = defaultState;
527 
528   // We have to be very careful here: if aNotify is false we do NOT
529   // want to call UpdateState, because that will call into LinkState()
530   // and try to start off loads, etc.  But ResetLinkState is called
531   // with aNotify false when things are in inconsistent states, so
532   // we'll get confused in that situation.  Instead, just silently
533   // update the link state on mElement. Since we might have set the
534   // link state to unvisited, make sure to update with that state if
535   // required.
536   if (aNotify) {
537     mElement->UpdateState(aNotify);
538   } else {
539     if (mLinkState == eLinkState_Unvisited) {
540       mElement->UpdateLinkState(NS_EVENT_STATE_UNVISITED);
541     } else {
542       mElement->UpdateLinkState(EventStates());
543     }
544   }
545 }
546 
UnregisterFromHistory()547 void Link::UnregisterFromHistory() {
548   // If we are not registered, we have nothing to do.
549   if (!mRegistered) {
550     return;
551   }
552 
553   // And tell History to stop tracking us.
554   if (mHistory && mCachedURI) {
555     if (nsCOMPtr<IHistory> history = services::GetHistoryService()) {
556       history->UnregisterVisitedCallback(mCachedURI, this);
557       mRegistered = false;
558     }
559   }
560 }
561 
SetHrefAttribute(nsIURI * aURI)562 void Link::SetHrefAttribute(nsIURI* aURI) {
563   NS_ASSERTION(aURI, "Null URI is illegal!");
564 
565   // if we change this code to not reserialize we need to do something smarter
566   // in SetProtocol because changing the protocol of an URI can change the
567   // "nature" of the nsIURL/nsIURI implementation.
568   nsAutoCString href;
569   (void)aURI->GetSpec(href);
570   (void)mElement->SetAttr(kNameSpaceID_None, nsGkAtoms::href,
571                           NS_ConvertUTF8toUTF16(href), true);
572 }
573 
SizeOfExcludingThis(mozilla::SizeOfState & aState) const574 size_t Link::SizeOfExcludingThis(mozilla::SizeOfState& aState) const {
575   size_t n = 0;
576 
577   if (mCachedURI) {
578     nsCOMPtr<nsISizeOf> iface = do_QueryInterface(mCachedURI);
579     if (iface) {
580       n += iface->SizeOfIncludingThis(aState.mMallocSizeOf);
581     }
582   }
583 
584   // The following members don't need to be measured:
585   // - mElement, because it is a pointer-to-self used to avoid QIs
586 
587   return n;
588 }
589 
590 }  // namespace dom
591 }  // namespace mozilla
592