GeckoViewHistory.cpp (15665B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 #include "GeckoViewHistory.h" 6 7 #include "jsapi.h" 8 #include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject 9 #include "js/PropertyAndElement.h" // JS_GetElement 10 #include "nsIURI.h" 11 #include "nsXULAppAPI.h" 12 13 #include "mozilla/ClearOnShutdown.h" 14 #include "mozilla/StaticPrefs_layout.h" 15 16 #include "mozilla/dom/ContentParent.h" 17 #include "mozilla/dom/Element.h" 18 #include "mozilla/dom/Link.h" 19 #include "mozilla/dom/BrowserChild.h" 20 21 #include "mozilla/ipc/URIUtils.h" 22 23 #include "mozilla/widget/EventDispatcher.h" 24 #include "mozilla/widget/nsWindow.h" 25 26 using namespace mozilla; 27 using namespace mozilla::dom; 28 using namespace mozilla::ipc; 29 using namespace mozilla::widget; 30 31 static const nsLiteralString kOnVisitedMessage = u"GeckoView:OnVisited"_ns; 32 static const nsLiteralString kGetVisitedMessage = u"GeckoView:GetVisited"_ns; 33 34 // Keep in sync with `GeckoSession.HistoryDelegate.VisitFlags`. 35 enum class GeckoViewVisitFlags : int32_t { 36 VISIT_TOP_LEVEL = 1 << 0, 37 VISIT_REDIRECT_TEMPORARY = 1 << 1, 38 VISIT_REDIRECT_PERMANENT = 1 << 2, 39 VISIT_REDIRECT_SOURCE = 1 << 3, 40 VISIT_REDIRECT_SOURCE_PERMANENT = 1 << 4, 41 VISIT_UNRECOVERABLE_ERROR = 1 << 5, 42 }; 43 44 GeckoViewHistory::GeckoViewHistory() {} 45 46 GeckoViewHistory::~GeckoViewHistory() {} 47 48 NS_IMPL_ISUPPORTS(GeckoViewHistory, IHistory) 49 50 StaticRefPtr<GeckoViewHistory> GeckoViewHistory::sHistory; 51 52 /* static */ 53 already_AddRefed<GeckoViewHistory> GeckoViewHistory::GetSingleton() { 54 if (!sHistory) { 55 sHistory = new GeckoViewHistory(); 56 ClearOnShutdown(&sHistory); 57 } 58 RefPtr<GeckoViewHistory> history = sHistory; 59 return history.forget(); 60 } 61 62 // Handles a request to fetch visited statuses for new tracked URIs in the 63 // content process (e10s). 64 void GeckoViewHistory::QueryVisitedStateInContentProcess( 65 const PendingVisitedQueries& aQueries) { 66 // Holds an array of new tracked URIs for a tab in the content process. 67 struct NewURIEntry { 68 explicit NewURIEntry(BrowserChild* aBrowserChild, nsIURI* aURI) 69 : mBrowserChild(aBrowserChild) { 70 AddURI(aURI); 71 } 72 73 void AddURI(nsIURI* aURI) { mURIs.AppendElement(aURI); } 74 75 BrowserChild* mBrowserChild; 76 nsTArray<RefPtr<nsIURI>> mURIs; 77 }; 78 79 MOZ_ASSERT(XRE_IsContentProcess()); 80 81 // First, serialize all the new URIs that we need to look up. Note that this 82 // could be written as `nsTHashMap<nsUint64HashKey, 83 // nsTArray<URIParams>` instead, but, since we don't expect to have many tab 84 // children, we can avoid the cost of hashing. 85 AutoTArray<NewURIEntry, 8> newEntries; 86 for (auto& query : aQueries) { 87 nsIURI* uri = query.GetKey(); 88 MOZ_ASSERT(query.GetData().IsEmpty(), 89 "Shouldn't have parents to notify in child processes"); 90 auto entry = mTrackedURIs.Lookup(uri); 91 if (!entry) { 92 continue; 93 } 94 ObservingLinks& links = entry.Data(); 95 for (Link* link : links.mLinks.BackwardRange()) { 96 nsIWidget* widget = nsContentUtils::WidgetForContent(link->GetElement()); 97 if (!widget) { 98 continue; 99 } 100 BrowserChild* browserChild = widget->GetOwningBrowserChild(); 101 if (!browserChild) { 102 continue; 103 } 104 // Add to the list of new URIs for this document, or make a new entry. 105 bool hasEntry = false; 106 for (NewURIEntry& entry : newEntries) { 107 if (entry.mBrowserChild == browserChild) { 108 entry.AddURI(uri); 109 hasEntry = true; 110 break; 111 } 112 } 113 if (!hasEntry) { 114 newEntries.AppendElement(NewURIEntry(browserChild, uri)); 115 } 116 } 117 } 118 119 // Send the request to the parent process, one message per tab child. 120 for (const NewURIEntry& entry : newEntries) { 121 (void)NS_WARN_IF(!entry.mBrowserChild->SendQueryVisitedState(entry.mURIs)); 122 } 123 } 124 125 // Handles a request to fetch visited statuses for new tracked URIs in the 126 // parent process (non-e10s). 127 void GeckoViewHistory::QueryVisitedStateInParentProcess( 128 const PendingVisitedQueries& aQueries) { 129 // Holds an array of new URIs for a window in the parent process. Unlike 130 // the content process case, we don't need to track tab children, since we 131 // have the outer window and can send the request directly to Java. 132 struct NewURIEntry { 133 explicit NewURIEntry(nsIWidget* aWidget, nsIURI* aURI) : mWidget(aWidget) { 134 AddURI(aURI); 135 } 136 137 void AddURI(nsIURI* aURI) { mURIs.AppendElement(aURI); } 138 139 nsCOMPtr<nsIWidget> mWidget; 140 nsTArray<RefPtr<nsIURI>> mURIs; 141 }; 142 143 MOZ_ASSERT(XRE_IsParentProcess()); 144 145 nsTArray<NewURIEntry> newEntries; 146 for (const auto& query : aQueries) { 147 nsIURI* uri = query.GetKey(); 148 auto entry = mTrackedURIs.Lookup(uri); 149 if (!entry) { 150 continue; // Nobody cares about this uri anymore. 151 } 152 153 ObservingLinks& links = entry.Data(); 154 nsTObserverArray<Link*>::BackwardIterator linksIter(links.mLinks); 155 while (linksIter.HasMore()) { 156 Link* link = linksIter.GetNext(); 157 158 nsIWidget* widget = nsContentUtils::WidgetForContent(link->GetElement()); 159 if (!widget) { 160 continue; 161 } 162 163 bool hasEntry = false; 164 for (NewURIEntry& entry : newEntries) { 165 if (entry.mWidget != widget) { 166 continue; 167 } 168 entry.AddURI(uri); 169 hasEntry = true; 170 } 171 if (!hasEntry) { 172 newEntries.AppendElement(NewURIEntry(widget, uri)); 173 } 174 } 175 } 176 177 for (NewURIEntry& entry : newEntries) { 178 QueryVisitedState(entry.mWidget, nullptr, std::move(entry.mURIs)); 179 } 180 } 181 182 void GeckoViewHistory::StartPendingVisitedQueries( 183 PendingVisitedQueries&& aQueries) { 184 if (XRE_IsContentProcess()) { 185 QueryVisitedStateInContentProcess(aQueries); 186 } else { 187 QueryVisitedStateInParentProcess(aQueries); 188 } 189 } 190 191 /** 192 * Called from the session handler for the history delegate, after the new 193 * visit is recorded. 194 */ 195 class OnVisitedCallback final : public nsIGeckoViewEventCallback { 196 public: 197 explicit OnVisitedCallback(GeckoViewHistory* aHistory, nsIURI* aURI) 198 : mHistory(aHistory), mURI(aURI) {} 199 200 NS_DECL_ISUPPORTS 201 202 NS_IMETHOD 203 OnSuccess(JS::Handle<JS::Value> aData, JSContext* aCx) override { 204 Maybe<bool> visitedState = GetVisitedValue(aCx, aData); 205 JS_ClearPendingException(aCx); 206 if (visitedState) { 207 AutoTArray<VisitedURI, 1> visitedURIs; 208 visitedURIs.AppendElement(VisitedURI{mURI.get(), *visitedState}); 209 mHistory->HandleVisitedState(visitedURIs, nullptr); 210 } 211 return NS_OK; 212 } 213 214 NS_IMETHOD 215 OnError(JS::Handle<JS::Value> aData, JSContext* aCx) override { 216 return NS_OK; 217 } 218 219 private: 220 virtual ~OnVisitedCallback() {} 221 222 Maybe<bool> GetVisitedValue(JSContext* aCx, JS::Handle<JS::Value> aData) { 223 if (NS_WARN_IF(!aData.isBoolean())) { 224 return Nothing(); 225 } 226 return Some(aData.toBoolean()); 227 } 228 229 RefPtr<GeckoViewHistory> mHistory; 230 nsCOMPtr<nsIURI> mURI; 231 }; 232 233 NS_IMPL_ISUPPORTS(OnVisitedCallback, nsIGeckoViewEventCallback) 234 235 NS_IMETHODIMP 236 GeckoViewHistory::VisitURI(nsIWidget* aWidget, nsIURI* aURI, 237 nsIURI* aLastVisitedURI, uint32_t aFlags, 238 uint64_t aBrowserId) { 239 AssertIsOnMainThread(); 240 if (!aURI) { 241 return NS_OK; 242 } 243 244 if (XRE_IsContentProcess()) { 245 // If we're in the content process, send the visit to the parent. The parent 246 // will find the matching chrome window for the content process and tab, 247 // then forward the visit to Java. 248 if (NS_WARN_IF(!aWidget)) { 249 return NS_OK; 250 } 251 BrowserChild* browserChild = aWidget->GetOwningBrowserChild(); 252 if (NS_WARN_IF(!browserChild)) { 253 return NS_OK; 254 } 255 (void)NS_WARN_IF( 256 !browserChild->SendVisitURI(aURI, aLastVisitedURI, aFlags, aBrowserId)); 257 return NS_OK; 258 } 259 260 // Otherwise, we're in the parent process. Wrap the URIs up in a bundle, and 261 // send them to Java. 262 MOZ_ASSERT(XRE_IsParentProcess()); 263 RefPtr<nsWindow> window = nsWindow::From(aWidget); 264 if (NS_WARN_IF(!window)) { 265 return NS_OK; 266 } 267 widget::EventDispatcher* dispatcher = window->GetEventDispatcher(); 268 if (NS_WARN_IF(!dispatcher)) { 269 return NS_OK; 270 } 271 272 // If nobody is listening for this, we can stop now. 273 if (!dispatcher->HasEmbedderListener(kOnVisitedMessage)) { 274 return NS_OK; 275 } 276 277 dom::AutoJSAPI jsapi; 278 NS_ENSURE_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()), NS_OK); 279 280 JS::Rooted<JSObject*> bundle(jsapi.cx(), JS_NewPlainObject(jsapi.cx())); 281 NS_ENSURE_TRUE(bundle, NS_OK); 282 283 JS::Rooted<JS::Value> value(jsapi.cx()); 284 285 nsAutoCString uriSpec; 286 if (NS_WARN_IF(NS_FAILED(aURI->GetSpec(uriSpec)))) { 287 return NS_OK; 288 } 289 NS_ENSURE_TRUE(ToJSValue(jsapi.cx(), uriSpec, &value), NS_OK); 290 NS_ENSURE_TRUE(JS_SetProperty(jsapi.cx(), bundle, "url", value), NS_OK); 291 292 if (aLastVisitedURI) { 293 nsAutoCString lastVisitedURISpec; 294 if (NS_WARN_IF(NS_FAILED(aLastVisitedURI->GetSpec(lastVisitedURISpec)))) { 295 return NS_OK; 296 } 297 NS_ENSURE_TRUE(ToJSValue(jsapi.cx(), lastVisitedURISpec, &value), NS_OK); 298 NS_ENSURE_TRUE(JS_SetProperty(jsapi.cx(), bundle, "lastVisitedURL", value), 299 NS_OK); 300 } 301 302 int32_t flags = 0; 303 if (aFlags & TOP_LEVEL) { 304 flags |= static_cast<int32_t>(GeckoViewVisitFlags::VISIT_TOP_LEVEL); 305 } 306 if (aFlags & REDIRECT_TEMPORARY) { 307 flags |= 308 static_cast<int32_t>(GeckoViewVisitFlags::VISIT_REDIRECT_TEMPORARY); 309 } 310 if (aFlags & REDIRECT_PERMANENT) { 311 flags |= 312 static_cast<int32_t>(GeckoViewVisitFlags::VISIT_REDIRECT_PERMANENT); 313 } 314 if (aFlags & REDIRECT_SOURCE) { 315 flags |= static_cast<int32_t>(GeckoViewVisitFlags::VISIT_REDIRECT_SOURCE); 316 } 317 if (aFlags & REDIRECT_SOURCE_PERMANENT) { 318 flags |= static_cast<int32_t>( 319 GeckoViewVisitFlags::VISIT_REDIRECT_SOURCE_PERMANENT); 320 } 321 if (aFlags & UNRECOVERABLE_ERROR) { 322 flags |= 323 static_cast<int32_t>(GeckoViewVisitFlags::VISIT_UNRECOVERABLE_ERROR); 324 } 325 value = JS::Int32Value(flags); 326 NS_ENSURE_TRUE(JS_SetProperty(jsapi.cx(), bundle, "flags", value), NS_OK); 327 328 nsCOMPtr<nsIGeckoViewEventCallback> callback = 329 new OnVisitedCallback(this, aURI); 330 331 (void)NS_WARN_IF( 332 NS_FAILED(dispatcher->Dispatch(kOnVisitedMessage, bundle, callback))); 333 return NS_OK; 334 } 335 336 NS_IMETHODIMP 337 GeckoViewHistory::SetURITitle(nsIURI* aURI, const nsAString& aTitle) { 338 return NS_ERROR_NOT_IMPLEMENTED; 339 } 340 341 /** 342 * Called from the session handler for the history delegate, with visited 343 * statuses for all requested URIs. 344 */ 345 class GetVisitedCallback final : public nsIGeckoViewEventCallback { 346 public: 347 explicit GetVisitedCallback(GeckoViewHistory* aHistory, 348 ContentParent* aInterestedProcess, 349 nsTArray<RefPtr<nsIURI>>&& aURIs) 350 : mHistory(aHistory), 351 mInterestedProcess(aInterestedProcess), 352 mURIs(std::move(aURIs)) {} 353 354 NS_DECL_ISUPPORTS 355 356 NS_IMETHOD 357 OnSuccess(JS::Handle<JS::Value> aData, JSContext* aCx) override { 358 nsTArray<VisitedURI> visitedURIs; 359 if (!ExtractVisitedURIs(aCx, aData, visitedURIs)) { 360 JS_ClearPendingException(aCx); 361 return NS_ERROR_FAILURE; 362 } 363 IHistory::ContentParentSet interestedProcesses; 364 if (mInterestedProcess) { 365 interestedProcesses.Insert(mInterestedProcess); 366 } 367 mHistory->HandleVisitedState(visitedURIs, &interestedProcesses); 368 return NS_OK; 369 } 370 371 NS_IMETHOD 372 OnError(JS::Handle<JS::Value> aData, JSContext* aCx) override { 373 return NS_OK; 374 } 375 376 private: 377 virtual ~GetVisitedCallback() {} 378 379 /** 380 * Unpacks an array of Boolean visited statuses from the session handler into 381 * an array of `VisitedURI` structs. Each element in the array corresponds to 382 * a URI in `mURIs`. 383 * 384 * Returns `false` on error, `true` if the array is `null` or was successfully 385 * unpacked. 386 * 387 * TODO (bug 1503482): Remove this unboxing. 388 */ 389 bool ExtractVisitedURIs(JSContext* aCx, JS::Handle<JS::Value> aData, 390 nsTArray<VisitedURI>& aVisitedURIs) { 391 if (aData.isNull()) { 392 return true; 393 } 394 bool isArray = false; 395 if (NS_WARN_IF(!JS::IsArrayObject(aCx, aData, &isArray))) { 396 return false; 397 } 398 if (NS_WARN_IF(!isArray)) { 399 return false; 400 } 401 JS::Rooted<JSObject*> visited(aCx, &aData.toObject()); 402 uint32_t length = 0; 403 if (NS_WARN_IF(!JS::GetArrayLength(aCx, visited, &length))) { 404 return false; 405 } 406 if (NS_WARN_IF(length != mURIs.Length())) { 407 return false; 408 } 409 if (!aVisitedURIs.SetCapacity(length, mozilla::fallible)) { 410 return false; 411 } 412 for (uint32_t i = 0; i < length; ++i) { 413 JS::Rooted<JS::Value> value(aCx); 414 if (NS_WARN_IF(!JS_GetElement(aCx, visited, i, &value))) { 415 JS_ClearPendingException(aCx); 416 aVisitedURIs.AppendElement(VisitedURI{mURIs[i].get(), false}); 417 continue; 418 } 419 if (NS_WARN_IF(!value.isBoolean())) { 420 aVisitedURIs.AppendElement(VisitedURI{mURIs[i].get(), false}); 421 continue; 422 } 423 aVisitedURIs.AppendElement(VisitedURI{mURIs[i].get(), value.toBoolean()}); 424 } 425 return true; 426 } 427 428 RefPtr<GeckoViewHistory> mHistory; 429 RefPtr<ContentParent> mInterestedProcess; 430 nsTArray<RefPtr<nsIURI>> mURIs; 431 }; 432 433 NS_IMPL_ISUPPORTS(GetVisitedCallback, nsIGeckoViewEventCallback) 434 435 /** 436 * Queries the history delegate to find which URIs have been visited. This 437 * is always called in the parent process: from `GetVisited` in non-e10s, and 438 * from `ContentParent::RecvGetVisited` in e10s. 439 */ 440 void GeckoViewHistory::QueryVisitedState(nsIWidget* aWidget, 441 ContentParent* aInterestedProcess, 442 nsTArray<RefPtr<nsIURI>>&& aURIs) { 443 MOZ_ASSERT(XRE_IsParentProcess()); 444 AssertIsOnMainThread(); 445 446 RefPtr<nsWindow> window = nsWindow::From(aWidget); 447 if (NS_WARN_IF(!window)) { 448 return; 449 } 450 widget::EventDispatcher* dispatcher = window->GetEventDispatcher(); 451 if (NS_WARN_IF(!dispatcher)) { 452 return; 453 } 454 455 // If nobody is listening for this we can stop now 456 if (!dispatcher->HasEmbedderListener(kGetVisitedMessage)) { 457 return; 458 } 459 460 dom::AutoJSAPI jsapi; 461 NS_ENSURE_TRUE_VOID(jsapi.Init(xpc::PrivilegedJunkScope())); 462 463 nsTArray<nsCString> specs(aURIs.Length()); 464 for (auto& uri : aURIs) { 465 nsAutoCString uriSpec; 466 if (NS_WARN_IF(NS_FAILED(uri->GetSpec(uriSpec)))) { 467 continue; 468 } 469 specs.AppendElement(uriSpec); 470 } 471 472 JS::Rooted<JS::Value> urls(jsapi.cx()); 473 NS_ENSURE_TRUE_VOID(ToJSValue(jsapi.cx(), specs, &urls)); 474 475 JS::Rooted<JSObject*> bundle(jsapi.cx(), JS_NewPlainObject(jsapi.cx())); 476 NS_ENSURE_TRUE_VOID(bundle); 477 NS_ENSURE_TRUE_VOID(JS_SetProperty(jsapi.cx(), bundle, "urls", urls)); 478 479 nsCOMPtr<nsIGeckoViewEventCallback> callback = 480 new GetVisitedCallback(this, aInterestedProcess, std::move(aURIs)); 481 482 (void)NS_WARN_IF( 483 NS_FAILED(dispatcher->Dispatch(kGetVisitedMessage, bundle, callback))); 484 } 485 486 /** 487 * Updates link states for all tracked links, forwarding the visited statuses to 488 * the content process in e10s. This is always called in the parent process, 489 * from `VisitedCallback::OnSuccess` and `GetVisitedCallback::OnSuccess`. 490 */ 491 void GeckoViewHistory::HandleVisitedState( 492 const nsTArray<VisitedURI>& aVisitedURIs, 493 ContentParentSet* aInterestedProcesses) { 494 MOZ_ASSERT(XRE_IsParentProcess()); 495 496 for (const VisitedURI& visitedURI : aVisitedURIs) { 497 auto status = 498 visitedURI.mVisited ? VisitedStatus::Visited : VisitedStatus::Unvisited; 499 NotifyVisited(visitedURI.mURI, status, aInterestedProcesses); 500 } 501 }