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