ClientOpenWindowUtils.cpp (19298B)
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 "ClientOpenWindowUtils.h" 8 9 #include "ClientInfo.h" 10 #include "ClientManager.h" 11 #include "ClientState.h" 12 #include "mozilla/NullPrincipal.h" 13 #include "mozilla/dom/BrowserParent.h" 14 #include "mozilla/dom/BrowsingContext.h" 15 #include "mozilla/dom/CanonicalBrowsingContext.h" 16 #include "mozilla/dom/ContentParent.h" 17 #include "mozilla/dom/PolicyContainer.h" 18 #include "mozilla/dom/WindowGlobalParent.h" 19 #include "mozilla/dom/nsCSPContext.h" 20 #include "nsContentUtils.h" 21 #include "nsDocShell.h" 22 #include "nsDocShellLoadState.h" 23 #include "nsFocusManager.h" 24 #include "nsGlobalWindowOuter.h" 25 #include "nsIBrowser.h" 26 #include "nsIBrowserDOMWindow.h" 27 #include "nsIDocShell.h" 28 #include "nsIDocShellTreeOwner.h" 29 #include "nsIMutableArray.h" 30 #include "nsISupportsPrimitives.h" 31 #include "nsIURI.h" 32 #include "nsIWebProgress.h" 33 #include "nsIWebProgressListener.h" 34 #include "nsIWindowMediator.h" 35 #include "nsIWindowWatcher.h" 36 #include "nsIXPConnect.h" 37 #include "nsNetUtil.h" 38 #include "nsOpenWindowInfo.h" 39 #include "nsPIDOMWindow.h" 40 #include "nsPIWindowWatcher.h" 41 #include "nsPrintfCString.h" 42 #include "nsWindowWatcher.h" 43 44 #ifdef MOZ_GECKOVIEW 45 # include "mozilla/dom/Promise-inl.h" 46 # include "nsIGeckoViewServiceWorker.h" 47 # include "nsImportModule.h" 48 #endif 49 50 namespace mozilla::dom { 51 52 namespace { 53 54 class WebProgressListener final : public nsIWebProgressListener, 55 public nsSupportsWeakReference { 56 public: 57 NS_DECL_ISUPPORTS 58 59 WebProgressListener(BrowsingContext* aBrowsingContext, nsIURI* aBaseURI, 60 already_AddRefed<ClientOpPromise::Private> aPromise) 61 : mPromise(aPromise), 62 mBaseURI(aBaseURI), 63 mBrowserId(aBrowsingContext->GetBrowserId()) { 64 MOZ_ASSERT(mBrowserId != 0); 65 MOZ_ASSERT(aBaseURI); 66 MOZ_ASSERT(NS_IsMainThread()); 67 } 68 69 NS_IMETHOD 70 OnStateChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, 71 uint32_t aStateFlags, nsresult aStatus) override { 72 if (!(aStateFlags & STATE_IS_WINDOW) || 73 !(aStateFlags & (STATE_STOP | STATE_TRANSFERRING))) { 74 return NS_OK; 75 } 76 77 // Our browsing context may have been discarded before finishing the load, 78 // this is a navigation error. 79 RefPtr<CanonicalBrowsingContext> browsingContext = 80 CanonicalBrowsingContext::Cast( 81 BrowsingContext::GetCurrentTopByBrowserId(mBrowserId)); 82 if (!browsingContext || browsingContext->IsDiscarded()) { 83 CopyableErrorResult rv; 84 rv.ThrowInvalidStateError("Unable to open window"); 85 mPromise->Reject(rv, __func__); 86 mPromise = nullptr; 87 return NS_OK; 88 } 89 90 // Our caller keeps a strong reference, so it is safe to remove the listener 91 // from the BrowsingContext's nsIWebProgress. 92 auto RemoveListener = [&] { 93 nsCOMPtr<nsIWebProgress> webProgress = browsingContext->GetWebProgress(); 94 webProgress->RemoveProgressListener(this); 95 }; 96 97 RefPtr<dom::WindowGlobalParent> wgp = 98 browsingContext->GetCurrentWindowGlobal(); 99 if (NS_WARN_IF(!wgp)) { 100 CopyableErrorResult rv; 101 rv.ThrowInvalidStateError("Unable to open window"); 102 mPromise->Reject(rv, __func__); 103 mPromise = nullptr; 104 RemoveListener(); 105 return NS_OK; 106 } 107 108 if (NS_WARN_IF(wgp->IsInitialDocument())) { 109 // This is the load of the initial document, which is not the document we 110 // care about for the purposes of checking same-originness of the URL. 111 return NS_OK; 112 } 113 114 RemoveListener(); 115 116 // Check same origin. If the origins do not match, resolve with null (per 117 // step 7.2.7.1 of the openWindow spec). 118 nsCOMPtr<nsIScriptSecurityManager> securityManager = 119 nsContentUtils::GetSecurityManager(); 120 bool isPrivateWin = 121 wgp->DocumentPrincipal()->OriginAttributesRef().IsPrivateBrowsing(); 122 nsresult rv = securityManager->CheckSameOriginURI( 123 wgp->GetDocumentURI(), mBaseURI, false, isPrivateWin); 124 if (NS_WARN_IF(NS_FAILED(rv))) { 125 mPromise->Resolve(CopyableErrorResult(), __func__); 126 mPromise = nullptr; 127 return NS_OK; 128 } 129 130 Maybe<ClientInfo> info = wgp->GetClientInfo(); 131 if (info.isNothing()) { 132 CopyableErrorResult rv; 133 rv.ThrowInvalidStateError("Unable to open window"); 134 mPromise->Reject(rv, __func__); 135 mPromise = nullptr; 136 return NS_OK; 137 } 138 139 const nsID& id = info.ref().Id(); 140 const mozilla::ipc::PrincipalInfo& principal = info.ref().PrincipalInfo(); 141 ClientManager::GetInfoAndState(ClientGetInfoAndStateArgs(id, principal), 142 GetCurrentSerialEventTarget()) 143 ->ChainTo(mPromise.forget(), __func__); 144 145 return NS_OK; 146 } 147 148 NS_IMETHOD 149 OnProgressChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, 150 int32_t aCurSelfProgress, int32_t aMaxSelfProgress, 151 int32_t aCurTotalProgress, 152 int32_t aMaxTotalProgress) override { 153 MOZ_ASSERT(false, "Unexpected notification."); 154 return NS_OK; 155 } 156 157 NS_IMETHOD 158 OnLocationChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, 159 nsIURI* aLocation, uint32_t aFlags) override { 160 MOZ_ASSERT(false, "Unexpected notification."); 161 return NS_OK; 162 } 163 164 NS_IMETHOD 165 OnStatusChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, 166 nsresult aStatus, const char16_t* aMessage) override { 167 MOZ_ASSERT(false, "Unexpected notification."); 168 return NS_OK; 169 } 170 171 NS_IMETHOD 172 OnSecurityChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, 173 uint32_t aState) override { 174 MOZ_ASSERT(false, "Unexpected notification."); 175 return NS_OK; 176 } 177 178 NS_IMETHOD 179 OnContentBlockingEvent(nsIWebProgress* aWebProgress, nsIRequest* aRequest, 180 uint32_t aEvent) override { 181 MOZ_ASSERT(false, "Unexpected notification."); 182 return NS_OK; 183 } 184 185 private: 186 ~WebProgressListener() { 187 if (mPromise) { 188 CopyableErrorResult rv; 189 rv.ThrowAbortError("openWindow aborted"); 190 mPromise->Reject(rv, __func__); 191 mPromise = nullptr; 192 } 193 } 194 195 RefPtr<ClientOpPromise::Private> mPromise; 196 nsCOMPtr<nsIURI> mBaseURI; 197 uint64_t mBrowserId; 198 }; 199 200 NS_IMPL_ISUPPORTS(WebProgressListener, nsIWebProgressListener, 201 nsISupportsWeakReference); 202 203 struct ClientOpenWindowArgsParsed { 204 nsCOMPtr<nsIURI> uri; 205 nsCOMPtr<nsIURI> baseURI; 206 nsCOMPtr<nsIPrincipal> principal; 207 nsCOMPtr<nsIPolicyContainer> policyContainer; 208 RefPtr<ThreadsafeContentParentHandle> originContent; 209 }; 210 211 #ifndef MOZ_GECKOVIEW 212 213 static Result<Ok, nsresult> OpenNewWindow( 214 const ClientOpenWindowArgsParsed& aArgsValidated, 215 nsOpenWindowInfo* aOpenWindowInfo) { 216 nsresult rv; 217 218 // XXX(krosylight): In an ideal world we should be able to pass the nsIURI 219 // directly. See bug 1485961. 220 nsAutoCString uriToLoad; 221 MOZ_TRY(aArgsValidated.uri->GetSpec(uriToLoad)); 222 223 nsCOMPtr<nsISupportsCString> nsUriToLoad = 224 do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID, &rv); 225 MOZ_TRY(rv); 226 MOZ_TRY(nsUriToLoad->SetData(uriToLoad)); 227 228 nsCOMPtr<nsISupportsPRBool> nsFalse = 229 do_CreateInstance(NS_SUPPORTS_PRBOOL_CONTRACTID, &rv); 230 MOZ_TRY(rv); 231 MOZ_TRY(nsFalse->SetData(false)); 232 233 nsCOMPtr<nsISupportsPRUint32> userContextId = 234 do_CreateInstance(NS_SUPPORTS_PRUINT32_CONTRACTID, &rv); 235 MOZ_TRY(rv); 236 MOZ_TRY(userContextId->SetData(aArgsValidated.principal->GetUserContextId())); 237 238 nsCOMPtr<nsIMutableArray> args = do_CreateInstance(NS_ARRAY_CONTRACTID); 239 // https://searchfox.org/mozilla-central/rev/02d33f4bf984f65bd394bfd2d19d66569ae2cfe1/browser/base/content/browser-init.js#725-735 240 args->AppendElement(nsUriToLoad); // 0: uriToLoad 241 args->AppendElement(nullptr); // 1: extraOptions 242 args->AppendElement(nullptr); // 2: referrerInfo 243 args->AppendElement(nullptr); // 3: postData 244 args->AppendElement(nsFalse); // 4: allowThirdPartyFixup 245 args->AppendElement(userContextId); // 5: userContextId 246 args->AppendElement(nullptr); // 6: originPrincipal 247 args->AppendElement(nullptr); // 7: originStoragePrincipal 248 args->AppendElement(aArgsValidated.principal); // 8: triggeringPrincipal 249 args->AppendElement(nsFalse); // 9: allowInheritPrincipal 250 args->AppendElement(aArgsValidated.policyContainer); // 10: policyContainer 251 args->AppendElement(aOpenWindowInfo); // 11: nsOpenWindowInfo 252 253 nsCOMPtr<nsIWindowWatcher> ww = do_GetService(NS_WINDOWWATCHER_CONTRACTID); 254 nsCString features = "chrome,all,dialog=no"_ns; 255 256 if (aArgsValidated.principal->GetIsInPrivateBrowsing()) { 257 // Private browsing would generally have a window, but with 258 // browser.privatebrowsing.autostart=true it's still possible to get into 259 // this path. See also bug 1972335. 260 features += ",private"; 261 } 262 263 nsCOMPtr<mozIDOMWindowProxy> win; 264 MOZ_TRY(ww->OpenWindow(nullptr, nsDependentCString(BROWSER_CHROME_URL_QUOTED), 265 "_blank"_ns, features, args, getter_AddRefs(win))); 266 return Ok(); 267 } 268 269 /** 270 * @return true when the caller need to load URI on the resulting browsing 271 * context, otherwise false 272 */ 273 bool OpenWindow(const ClientOpenWindowArgsParsed& aArgsValidated, 274 nsOpenWindowInfo* aOpenInfo, BrowsingContext** aBC, 275 ErrorResult& aRv) { 276 MOZ_DIAGNOSTIC_ASSERT(aBC); 277 278 // [[6.1 Open Window]] 279 280 // Find the most recent browser window and open a new tab in it. 281 WindowMediatorFilter filter = WindowMediatorFilter::SkipClosed; 282 if (aArgsValidated.principal->GetIsInPrivateBrowsing()) { 283 filter |= WindowMediatorFilter::SkipNonPrivateBrowsing; 284 } else { 285 filter |= WindowMediatorFilter::SkipPrivateBrowsing; 286 } 287 nsCOMPtr<nsPIDOMWindowOuter> browserWindow = 288 nsContentUtils::GetMostRecentWindowBy(filter); 289 if (!browserWindow) { 290 // It is possible to be running without a browser window (either because 291 // macOS hidden window or non-browser windows like Library), so we need to 292 // open a new chrome window. 293 auto result = OpenNewWindow(aArgsValidated, aOpenInfo); 294 if (NS_WARN_IF(result.isErr())) { 295 aRv.ThrowTypeError("Unable to open window"); 296 return false; 297 } 298 return false; 299 } 300 301 if (NS_WARN_IF(!nsGlobalWindowOuter::Cast(browserWindow)->IsChromeWindow())) { 302 // XXXbz Can this actually happen? Seems unlikely. 303 aRv.ThrowTypeError("Unable to open window"); 304 return false; 305 } 306 307 nsCOMPtr<nsIBrowserDOMWindow> bwin = 308 nsGlobalWindowOuter::Cast(browserWindow)->GetBrowserDOMWindow(); 309 310 if (NS_WARN_IF(!bwin)) { 311 aRv.ThrowTypeError("Unable to open window"); 312 return false; 313 } 314 nsresult rv = bwin->CreateContentWindow( 315 nullptr, aOpenInfo, nsIBrowserDOMWindow::OPEN_DEFAULTWINDOW, 316 nsIBrowserDOMWindow::OPEN_NEW, aArgsValidated.principal, 317 aArgsValidated.policyContainer, aBC); 318 if (NS_WARN_IF(NS_FAILED(rv))) { 319 aRv.ThrowTypeError("Unable to open window"); 320 return false; 321 } 322 return true; 323 } 324 #endif 325 326 void WaitForLoad(const ClientOpenWindowArgsParsed& aArgsValidated, 327 BrowsingContext* aBrowsingContext, 328 ClientOpPromise::Private* aPromise, bool aShouldLoadURI) { 329 MOZ_DIAGNOSTIC_ASSERT(aBrowsingContext); 330 331 RefPtr<ClientOpPromise::Private> promise = aPromise; 332 // We can get a WebProgress off of 333 // the BrowsingContext for the <xul:browser> to listen for content 334 // events. Note that this WebProgress filters out events which don't have 335 // STATE_IS_NETWORK or STATE_IS_REDIRECTED_DOCUMENT set on them, and so this 336 // listener will only see some web progress events. 337 nsCOMPtr<nsIWebProgress> webProgress = 338 aBrowsingContext->Canonical()->GetWebProgress(); 339 if (NS_WARN_IF(!webProgress)) { 340 CopyableErrorResult result; 341 result.ThrowInvalidStateError("Unable to watch window for navigation"); 342 promise->Reject(result, __func__); 343 return; 344 } 345 346 // Add a progress listener before we start the load of the requested URI 347 RefPtr<WebProgressListener> listener = new WebProgressListener( 348 aBrowsingContext, aArgsValidated.baseURI, do_AddRef(promise)); 349 350 nsresult rv = webProgress->AddProgressListener( 351 listener, nsIWebProgress::NOTIFY_STATE_WINDOW); 352 if (NS_WARN_IF(NS_FAILED(rv))) { 353 CopyableErrorResult result; 354 // XXXbz Can we throw something better here? 355 result.Throw(rv); 356 promise->Reject(result, __func__); 357 return; 358 } 359 360 if (aShouldLoadURI) { 361 // Load the requested URI 362 RefPtr<nsDocShellLoadState> loadState = 363 new nsDocShellLoadState(aArgsValidated.uri); 364 loadState->SetTriggeringPrincipal(aArgsValidated.principal); 365 loadState->SetFirstParty(true); 366 loadState->SetLoadFlags( 367 nsIWebNavigation::LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL); 368 loadState->SetTriggeringRemoteType( 369 aArgsValidated.originContent 370 ? aArgsValidated.originContent->GetRemoteType() 371 : NOT_REMOTE_TYPE); 372 373 rv = aBrowsingContext->LoadURI(loadState, true); 374 if (NS_FAILED(rv)) { 375 CopyableErrorResult result; 376 result.ThrowInvalidStateError( 377 "Unable to start the load of the actual URI"); 378 promise->Reject(result, __func__); 379 return; 380 } 381 } 382 383 // Hold the listener alive until the promise settles. 384 promise->Then( 385 GetMainThreadSerialEventTarget(), __func__, 386 [listener](const ClientOpResult& aResult) {}, 387 [listener](const CopyableErrorResult& aResult) {}); 388 } 389 390 #ifdef MOZ_GECKOVIEW 391 void GeckoViewOpenWindow(const ClientOpenWindowArgsParsed& aArgsValidated, 392 nsOpenWindowInfo* aOpenInfo, ErrorResult& aRv) { 393 MOZ_ASSERT(aOpenInfo); 394 395 // passes the request to open a new window to GeckoView. Allowing the 396 // application to decide how to hand the open window request. 397 nsCOMPtr<nsIGeckoViewServiceWorker> sw = do_ImportESModule( 398 "resource://gre/modules/GeckoViewServiceWorker.sys.mjs"); 399 MOZ_ASSERT(sw); 400 401 RefPtr<dom::Promise> promise; 402 nsresult rv = 403 sw->OpenWindow(aArgsValidated.uri, aOpenInfo, getter_AddRefs(promise)); 404 if (NS_WARN_IF(NS_FAILED(rv))) { 405 aRv.Throw(rv); 406 return; 407 } 408 409 promise->AddCallbacksWithCycleCollectedArgs( 410 [](JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv, 411 nsOpenWindowInfo* aOpenWindowInfo) { 412 if (aValue.isNull()) { 413 // nsIBrowsingContextReadyCallback will be called when browsing 414 // context is ready 415 return; 416 } 417 418 auto cancelOpen = 419 MakeScopeExit([&aOpenWindowInfo] { aOpenWindowInfo->Cancel(); }); 420 421 RefPtr<BrowsingContext> browsingContext; 422 if (NS_WARN_IF(!aValue.isObject()) || 423 NS_WARN_IF(NS_FAILED( 424 UNWRAP_OBJECT(BrowsingContext, aValue, browsingContext)))) { 425 return; 426 } 427 428 if (nsIBrowsingContextReadyCallback* callback = 429 aOpenWindowInfo->BrowsingContextReadyCallback()) { 430 callback->BrowsingContextReady(browsingContext); 431 } 432 cancelOpen.release(); 433 }, 434 [](JSContext* aContext, JS::Handle<JS::Value> aValue, ErrorResult& aRv, 435 nsOpenWindowInfo* aOpenWindowInfo) { aOpenWindowInfo->Cancel(); }, 436 RefPtr(aOpenInfo)); 437 } 438 #endif // MOZ_GECKOVIEW 439 440 } // anonymous namespace 441 442 RefPtr<ClientOpPromise> ClientOpenWindow( 443 ThreadsafeContentParentHandle* aOriginContent, 444 const ClientOpenWindowArgs& aArgs) { 445 MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess()); 446 447 RefPtr<ClientOpPromise::Private> promise = 448 new ClientOpPromise::Private(__func__); 449 450 // [[1. Let url be the result of parsing url with entry settings object's API 451 // base URL.]] 452 nsCOMPtr<nsIURI> baseURI; 453 nsresult rv = NS_NewURI(getter_AddRefs(baseURI), aArgs.baseURL()); 454 if (NS_WARN_IF(NS_FAILED(rv))) { 455 nsPrintfCString err("Invalid base URL \"%s\"", aArgs.baseURL().get()); 456 CopyableErrorResult errResult; 457 errResult.ThrowTypeError(err); 458 promise->Reject(errResult, __func__); 459 return promise; 460 } 461 462 nsCOMPtr<nsIURI> uri; 463 rv = NS_NewURI(getter_AddRefs(uri), aArgs.url(), nullptr, baseURI); 464 if (NS_WARN_IF(NS_FAILED(rv))) { 465 nsPrintfCString err("Invalid URL \"%s\"", aArgs.url().get()); 466 CopyableErrorResult errResult; 467 errResult.ThrowTypeError(err); 468 promise->Reject(errResult, __func__); 469 return promise; 470 } 471 472 auto principalOrErr = PrincipalInfoToPrincipal(aArgs.principalInfo()); 473 if (NS_WARN_IF(principalOrErr.isErr())) { 474 CopyableErrorResult errResult; 475 errResult.ThrowTypeError("Failed to obtain principal"); 476 promise->Reject(errResult, __func__); 477 return promise; 478 } 479 nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap(); 480 MOZ_DIAGNOSTIC_ASSERT(principal); 481 482 nsCOMPtr<nsIContentSecurityPolicy> csp; 483 nsCOMPtr<PolicyContainer> policyContainer; 484 if (aArgs.cspInfo().isSome()) { 485 csp = CSPInfoToCSP(aArgs.cspInfo().ref(), nullptr); 486 policyContainer = new PolicyContainer(); 487 PolicyContainer::Cast(policyContainer)->SetCSP(csp); 488 } 489 ClientOpenWindowArgsParsed argsValidated{ 490 .uri = uri, 491 .baseURI = baseURI, 492 .principal = principal, 493 .policyContainer = policyContainer, 494 .originContent = aOriginContent, 495 }; 496 497 RefPtr<BrowsingContextCallbackReceivedPromise::Private> 498 browsingContextReadyPromise = 499 new BrowsingContextCallbackReceivedPromise::Private(__func__); 500 RefPtr<nsIBrowsingContextReadyCallback> callback = 501 new nsBrowsingContextReadyCallback(browsingContextReadyPromise); 502 503 RefPtr<nsOpenWindowInfo> openInfo = new nsOpenWindowInfo(); 504 openInfo->mBrowsingContextReadyCallback = callback; 505 nsCOMPtr<nsIURI> nullPrincipalURI = NullPrincipal::CreateURI(nullptr); 506 nsCOMPtr<nsIPrincipal> initialPrincipal = 507 NullPrincipal::Create(principal->OriginAttributesRef(), nullPrincipalURI); 508 openInfo->mPrincipalToInheritForAboutBlank = initialPrincipal; 509 openInfo->mIsRemote = true; 510 511 RefPtr<BrowsingContext> bc; 512 IgnoredErrorResult errResult; 513 bool shouldLoadURI = true; 514 #ifdef MOZ_GECKOVIEW 515 // GeckoView has a delegation for service worker window. 516 GeckoViewOpenWindow(argsValidated, openInfo, errResult); 517 #else 518 shouldLoadURI = 519 OpenWindow(argsValidated, openInfo, getter_AddRefs(bc), errResult); 520 #endif 521 if (NS_WARN_IF(errResult.Failed())) { 522 promise->Reject(errResult, __func__); 523 return promise; 524 } 525 526 browsingContextReadyPromise->Then( 527 GetCurrentSerialEventTarget(), __func__, 528 [argsValidated, promise, 529 shouldLoadURI](const RefPtr<BrowsingContext>& aBC) { 530 WaitForLoad(argsValidated, aBC, promise, shouldLoadURI); 531 }, 532 [promise]() { 533 // in case of failure, reject the original promise 534 CopyableErrorResult result; 535 result.ThrowTypeError("Unable to open window"); 536 promise->Reject(result, __func__); 537 }); 538 if (bc) { 539 browsingContextReadyPromise->Resolve(bc, __func__); 540 } 541 return promise; 542 } 543 544 } // namespace mozilla::dom