IDTracker.cpp (10390B)
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 "IDTracker.h" 8 9 #include "mozilla/Encoding.h" 10 #include "mozilla/dom/Document.h" 11 #include "mozilla/dom/DocumentOrShadowRoot.h" 12 #include "mozilla/dom/SVGUseElement.h" 13 #include "mozilla/dom/ShadowRoot.h" 14 #include "nsAtom.h" 15 #include "nsContentUtils.h" 16 #include "nsCycleCollectionParticipant.h" 17 #include "nsEscape.h" 18 #include "nsIReferrerInfo.h" 19 #include "nsIURI.h" 20 #include "nsStringFwd.h" 21 22 namespace mozilla::dom { 23 24 static Element* LookupElement(DocumentOrShadowRoot& aDocOrShadow, nsAtom* aRef, 25 bool aReferenceImage) { 26 if (aReferenceImage) { 27 return aDocOrShadow.LookupImageElement(aRef); 28 } 29 return aDocOrShadow.GetElementById(aRef); 30 } 31 32 static DocumentOrShadowRoot* FindTreeToWatch(nsIContent& aContent, nsAtom* aID, 33 bool aReferenceImage) { 34 ShadowRoot* shadow = aContent.GetContainingShadow(); 35 36 // We allow looking outside an <svg:use> shadow tree for backwards compat. 37 while (shadow && shadow->Host()->IsSVGElement(nsGkAtoms::use)) { 38 // <svg:use> shadow trees are immutable, so we can just early-out if we find 39 // our relevant element instead of having to support watching multiple 40 // trees. 41 if (LookupElement(*shadow, aID, aReferenceImage)) { 42 return shadow; 43 } 44 shadow = shadow->Host()->GetContainingShadow(); 45 } 46 47 if (shadow) { 48 return shadow; 49 } 50 51 return aContent.OwnerDoc(); 52 } 53 54 IDTracker::IDTracker() = default; 55 56 IDTracker::~IDTracker() { Unlink(); } 57 58 void IDTracker::ResetToURIWithFragmentID(Element& aFrom, nsIURI* aURI, 59 nsIReferrerInfo* aReferrerInfo, 60 bool aReferenceImage) { 61 Unlink(); 62 63 if (!aURI) { 64 return; 65 } 66 67 nsAutoCString refPart; 68 aURI->GetRef(refPart); 69 // Unescape %-escapes in the reference. The result will be in the 70 // document charset, hopefully... 71 NS_UnescapeURL(refPart); 72 73 // Get the thing to observe changes to. 74 Document* doc = aFrom.OwnerDoc(); 75 auto encoding = doc->GetDocumentCharacterSet(); 76 77 nsAutoString ref; 78 nsresult rv = encoding->DecodeWithoutBOMHandling(refPart, ref); 79 if (NS_FAILED(rv) || ref.IsEmpty()) { 80 return; 81 } 82 83 bool isEqualExceptRef; 84 rv = aURI->EqualsExceptRef(doc->GetDocumentURI(), &isEqualExceptRef); 85 RefPtr<nsAtom> refAtom = NS_Atomize(ref); 86 if (NS_FAILED(rv) || !isEqualExceptRef) { 87 return ResetToExternalResource(aURI, aReferrerInfo, refAtom, aFrom, 88 aReferenceImage); 89 } 90 ResetToID(aFrom, refAtom, aReferenceImage); 91 } 92 93 void IDTracker::ResetToExternalResource(nsIURI* aURI, 94 nsIReferrerInfo* aReferrerInfo, 95 nsAtom* aRef, Element& aFrom, 96 bool aReferenceImage) { 97 Unlink(); 98 99 RefPtr<Document::ExternalResourceLoad> load; 100 Document* resourceDoc = aFrom.OwnerDoc()->RequestExternalResource( 101 aURI, aReferrerInfo, &aFrom, getter_AddRefs(load)); 102 if (!resourceDoc) { 103 if (!load) { 104 // Nothing will ever happen here 105 return; 106 } 107 auto* observer = new DocumentLoadNotification(this, aRef); 108 mPendingNotification = observer; 109 load->AddObserver(observer); 110 } 111 112 mWatchID = aRef; 113 mReferencingImage = aReferenceImage; 114 HaveNewDocumentOrShadowRoot(resourceDoc, /* aWatch = */ true, mWatchID); 115 } 116 117 static nsIURI* GetExternalResourceURIIfNeeded(nsIURI* aBaseURI, 118 Element& aFrom) { 119 if (!aBaseURI) { 120 // We don't know where this URI came from. 121 return nullptr; 122 } 123 SVGUseElement* use = aFrom.GetContainingSVGUseShadowHost(); 124 if (!use) { 125 return nullptr; 126 } 127 Document* doc = use->GetSourceDocument(); 128 if (!doc || doc == aFrom.OwnerDoc()) { 129 return nullptr; 130 } 131 nsIURI* originalURI = doc->GetDocumentURI(); 132 if (!originalURI) { 133 return nullptr; 134 } 135 // Content is in a shadow tree of an external resource. If this URL was 136 // specified in the subtree referenced by the <use> element, then we want the 137 // fragment-only URL to resolve to an element from the resource document. 138 // Otherwise, the URL was specified somewhere in the document with the <use> 139 // element, and we want the fragment-only URL to resolve to an element in that 140 // document. 141 bool equals = false; 142 if (NS_FAILED(aBaseURI->EqualsExceptRef(originalURI, &equals)) || !equals) { 143 return nullptr; 144 } 145 return originalURI; 146 } 147 148 void IDTracker::ResetToLocalFragmentID(Element& aFrom, 149 const nsAString& aLocalRef, 150 nsIURI* aBaseURI, 151 nsIReferrerInfo* aReferrerInfo, 152 bool aReferenceImage) { 153 MOZ_ASSERT(nsContentUtils::IsLocalRefURL(aLocalRef)); 154 155 auto ref = Substring(aLocalRef, 1); 156 if (ref.IsEmpty()) { 157 Unlink(); 158 return; 159 } 160 161 nsAutoCString utf8Ref; 162 if (!AppendUTF16toUTF8(ref, utf8Ref, mozilla::fallible)) { 163 Unlink(); 164 return; 165 } 166 167 // Only unescape ASCII characters; if we were to unescape arbitrary bytes, 168 // we'd potentially end up with invalid UTF-8. 169 nsAutoCString unescaped; 170 bool appended; 171 if (NS_FAILED(NS_UnescapeURL(utf8Ref.BeginReading(), utf8Ref.Length(), 172 esc_OnlyASCII | esc_AlwaysCopy, unescaped, 173 appended, mozilla::fallible))) { 174 Unlink(); 175 return; 176 } 177 178 RefPtr<nsAtom> refAtom = NS_Atomize(unescaped); 179 if (nsIURI* resourceUri = GetExternalResourceURIIfNeeded(aBaseURI, aFrom)) { 180 return ResetToExternalResource(resourceUri, aReferrerInfo, refAtom, aFrom, 181 aReferenceImage); 182 } 183 ResetToID(aFrom, refAtom, aReferenceImage); 184 } 185 186 void IDTracker::ResetToID(Element& aFrom, nsAtom* aID, bool aReferenceImage) { 187 MOZ_ASSERT(aID); 188 189 Unlink(); 190 191 if (aID->IsEmpty()) { 192 return; 193 } 194 195 mWatchID = aID; 196 mReferencingImage = aReferenceImage; 197 198 DocumentOrShadowRoot* docOrShadow = 199 FindTreeToWatch(aFrom, aID, aReferenceImage); 200 HaveNewDocumentOrShadowRoot(docOrShadow, /*aWatch*/ true, aID); 201 } 202 203 void IDTracker::HaveNewDocumentOrShadowRoot(DocumentOrShadowRoot* aDocOrShadow, 204 bool aWatch, nsAtom* aID) { 205 if (aWatch) { 206 mWatchDocumentOrShadowRoot = nullptr; 207 if (aDocOrShadow) { 208 mWatchDocumentOrShadowRoot = &aDocOrShadow->AsNode(); 209 mElement = aDocOrShadow->AddIDTargetObserver(mWatchID, Observe, this, 210 mReferencingImage); 211 } 212 return; 213 } 214 215 if (!aDocOrShadow) { 216 return; 217 } 218 219 if (Element* e = LookupElement(*aDocOrShadow, aID, mReferencingImage)) { 220 mElement = e; 221 } 222 } 223 224 void IDTracker::Traverse(nsCycleCollectionTraversalCallback* aCB) { 225 NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(*aCB, "mWatchDocumentOrShadowRoot"); 226 aCB->NoteXPCOMChild(mWatchDocumentOrShadowRoot); 227 NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(*aCB, "mElement"); 228 aCB->NoteXPCOMChild(mElement); 229 } 230 231 void IDTracker::Unlink() { 232 if (mWatchID) { 233 if (DocumentOrShadowRoot* docOrShadow = GetWatchDocOrShadowRoot()) { 234 docOrShadow->RemoveIDTargetObserver(mWatchID, Observe, this, 235 mReferencingImage); 236 } 237 } 238 if (mPendingNotification) { 239 mPendingNotification->Clear(); 240 mPendingNotification = nullptr; 241 } 242 mWatchDocumentOrShadowRoot = nullptr; 243 mWatchID = nullptr; 244 mElement = nullptr; 245 mReferencingImage = false; 246 } 247 248 void IDTracker::ElementChanged(Element* aFrom, Element* aTo) { mElement = aTo; } 249 250 bool IDTracker::Observe(Element* aOldElement, Element* aNewElement, 251 void* aData) { 252 IDTracker* p = static_cast<IDTracker*>(aData); 253 if (p->mPendingNotification) { 254 p->mPendingNotification->SetTo(aNewElement); 255 } else { 256 NS_ASSERTION(aOldElement == p->mElement, "Failed to track content!"); 257 ChangeNotification* watcher = 258 new ChangeNotification(p, aOldElement, aNewElement); 259 p->mPendingNotification = watcher; 260 nsContentUtils::AddScriptRunner(watcher); 261 } 262 bool keepTracking = p->IsPersistent(); 263 if (!keepTracking) { 264 p->mWatchDocumentOrShadowRoot = nullptr; 265 p->mWatchID = nullptr; 266 } 267 return keepTracking; 268 } 269 270 IDTracker::ChangeNotification::ChangeNotification(IDTracker* aTarget, 271 Element* aFrom, Element* aTo) 272 : mozilla::Runnable("IDTracker::ChangeNotification"), 273 Notification(aTarget), 274 mFrom(aFrom), 275 mTo(aTo) {} 276 277 IDTracker::ChangeNotification::~ChangeNotification() = default; 278 279 void IDTracker::ChangeNotification::SetTo(Element* aTo) { mTo = aTo; } 280 281 void IDTracker::ChangeNotification::Clear() { 282 Notification::Clear(); 283 mFrom = nullptr; 284 mTo = nullptr; 285 } 286 287 NS_IMPL_ISUPPORTS_INHERITED0(IDTracker::ChangeNotification, mozilla::Runnable) 288 NS_IMPL_ISUPPORTS(IDTracker::DocumentLoadNotification, nsIObserver) 289 290 NS_IMETHODIMP 291 IDTracker::DocumentLoadNotification::Observe(nsISupports* aSubject, 292 const char* aTopic, 293 const char16_t* aData) { 294 NS_ASSERTION(!strcmp(aTopic, "external-resource-document-created"), 295 "Unexpected topic"); 296 if (mTarget) { 297 nsCOMPtr<Document> doc = do_QueryInterface(aSubject); 298 mTarget->mPendingNotification = nullptr; 299 NS_ASSERTION(!mTarget->mElement, "Why do we have content here?"); 300 // Keep watching if IsPersistent(). 301 mTarget->HaveNewDocumentOrShadowRoot(doc, mTarget->IsPersistent(), mRef); 302 mTarget->ElementChanged(nullptr, mTarget->mElement); 303 } 304 return NS_OK; 305 } 306 307 DocumentOrShadowRoot* IDTracker::GetWatchDocOrShadowRoot() const { 308 if (!mWatchDocumentOrShadowRoot) { 309 return nullptr; 310 } 311 MOZ_ASSERT(mWatchDocumentOrShadowRoot->IsDocument() || 312 mWatchDocumentOrShadowRoot->IsShadowRoot()); 313 if (ShadowRoot* shadow = ShadowRoot::FromNode(*mWatchDocumentOrShadowRoot)) { 314 return shadow; 315 } 316 return mWatchDocumentOrShadowRoot->AsDocument(); 317 } 318 319 } // namespace mozilla::dom