PointerLockManager.cpp (17305B)
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 "PointerLockManager.h" 8 9 #include "mozilla/AsyncEventDispatcher.h" 10 #include "mozilla/EventStateManager.h" 11 #include "mozilla/Logging.h" 12 #include "mozilla/PresShell.h" 13 #include "mozilla/ScopeExit.h" 14 #include "mozilla/StaticPrefs_full_screen_api.h" 15 #include "mozilla/dom/BindingDeclarations.h" 16 #include "mozilla/dom/BrowserChild.h" 17 #include "mozilla/dom/BrowserParent.h" 18 #include "mozilla/dom/BrowsingContext.h" 19 #include "mozilla/dom/CanonicalBrowsingContext.h" 20 #include "mozilla/dom/Document.h" 21 #include "mozilla/dom/Element.h" 22 #include "mozilla/dom/PointerEventHandler.h" 23 #include "mozilla/dom/WindowContext.h" 24 #include "nsCOMPtr.h" 25 #include "nsMenuPopupFrame.h" 26 #include "nsSandboxFlags.h" 27 28 mozilla::LazyLogModule gPointerLockLog("PointerLock"); 29 30 #define MOZ_POINTERLOCK_LOG(...) \ 31 MOZ_LOG(gPointerLockLog, mozilla::LogLevel::Debug, (__VA_ARGS__)) 32 33 namespace mozilla { 34 35 using mozilla::dom::BrowserChild; 36 using mozilla::dom::BrowserParent; 37 using mozilla::dom::BrowsingContext; 38 using mozilla::dom::CallerType; 39 using mozilla::dom::CanonicalBrowsingContext; 40 using mozilla::dom::Document; 41 using mozilla::dom::Element; 42 using mozilla::dom::WindowContext; 43 44 // Reference to the pointer locked element. 45 constinit static nsWeakPtr sLockedElement; 46 47 // Reference to the document which requested pointer lock. 48 constinit static nsWeakPtr sLockedDoc; 49 50 // Reference to the BrowserParent requested pointer lock. 51 static BrowserParent* sLockedRemoteTarget = nullptr; 52 53 /* static */ 54 bool PointerLockManager::sIsLocked = false; 55 56 /* static */ 57 already_AddRefed<dom::Element> PointerLockManager::GetLockedElement() { 58 nsCOMPtr<Element> element = do_QueryReferent(sLockedElement); 59 return element.forget(); 60 } 61 62 /* static */ 63 already_AddRefed<dom::Document> PointerLockManager::GetLockedDocument() { 64 nsCOMPtr<Document> document = do_QueryReferent(sLockedDoc); 65 return document.forget(); 66 } 67 68 /* static */ 69 BrowserParent* PointerLockManager::GetLockedRemoteTarget() { 70 MOZ_ASSERT(XRE_IsParentProcess()); 71 return sLockedRemoteTarget; 72 } 73 74 static void DispatchPointerLockChange(Document* aTarget) { 75 if (!aTarget) { 76 return; 77 } 78 79 MOZ_POINTERLOCK_LOG("Dispatch pointerlockchange event [document=0x%p]", 80 aTarget); 81 RefPtr<AsyncEventDispatcher> asyncDispatcher = 82 new AsyncEventDispatcher(aTarget, u"pointerlockchange"_ns, 83 CanBubble::eYes, ChromeOnlyDispatch::eNo); 84 asyncDispatcher->PostDOMEvent(); 85 } 86 87 static void DispatchPointerLockError(Document* aTarget, const char* aMessage) { 88 if (!aTarget) { 89 return; 90 } 91 92 MOZ_POINTERLOCK_LOG( 93 "Dispatch pointerlockerror event [document=0x%p, message=%s]", aTarget, 94 aMessage); 95 RefPtr<AsyncEventDispatcher> asyncDispatcher = 96 new AsyncEventDispatcher(aTarget, u"pointerlockerror"_ns, CanBubble::eYes, 97 ChromeOnlyDispatch::eNo); 98 asyncDispatcher->PostDOMEvent(); 99 nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns, 100 aTarget, nsContentUtils::eDOM_PROPERTIES, 101 aMessage); 102 } 103 104 static bool IsPopupOpened() { 105 // Check if any popup is open. 106 nsXULPopupManager* pm = nsXULPopupManager::GetInstance(); 107 if (!pm) { 108 return false; 109 } 110 111 nsTArray<nsMenuPopupFrame*> popups; 112 pm->GetVisiblePopups(popups, true); 113 114 for (nsMenuPopupFrame* popup : popups) { 115 if (popup->GetPopupType() != widget::PopupType::Tooltip) { 116 return true; 117 } 118 } 119 120 return false; 121 } 122 123 static const char* GetPointerLockError(Element* aElement, Element* aCurrentLock, 124 bool aNoFocusCheck = false) { 125 // Check if pointer lock pref is enabled 126 if (!StaticPrefs::full_screen_api_pointer_lock_enabled()) { 127 return "PointerLockDeniedDisabled"; 128 } 129 130 nsCOMPtr<Document> ownerDoc = aElement->OwnerDoc(); 131 if (aCurrentLock && aCurrentLock->OwnerDoc() != ownerDoc) { 132 return "PointerLockDeniedInUse"; 133 } 134 135 if (!aElement->IsInComposedDoc()) { 136 return "PointerLockDeniedNotInDocument"; 137 } 138 139 if (ownerDoc->GetSandboxFlags() & SANDBOXED_POINTER_LOCK) { 140 return "PointerLockDeniedSandboxed"; 141 } 142 143 // Check if the element is in a document with a docshell. 144 if (!ownerDoc->GetContainer()) { 145 return "PointerLockDeniedHidden"; 146 } 147 nsCOMPtr<nsPIDOMWindowOuter> ownerWindow = ownerDoc->GetWindow(); 148 if (!ownerWindow) { 149 return "PointerLockDeniedHidden"; 150 } 151 nsCOMPtr<nsPIDOMWindowInner> ownerInnerWindow = ownerDoc->GetInnerWindow(); 152 if (!ownerInnerWindow) { 153 return "PointerLockDeniedHidden"; 154 } 155 if (ownerWindow->GetCurrentInnerWindow() != ownerInnerWindow) { 156 return "PointerLockDeniedHidden"; 157 } 158 159 BrowsingContext* bc = ownerDoc->GetBrowsingContext(); 160 BrowsingContext* topBC = bc ? bc->Top() : nullptr; 161 WindowContext* topWC = ownerDoc->GetTopLevelWindowContext(); 162 if (!topBC || !topBC->IsActive() || !topWC || 163 topWC != topBC->GetCurrentWindowContext()) { 164 return "PointerLockDeniedHidden"; 165 } 166 167 if (!aNoFocusCheck) { 168 if (!IsInActiveTab(ownerDoc)) { 169 return "PointerLockDeniedNotFocused"; 170 } 171 } 172 173 if (IsPopupOpened()) { 174 return "PointerLockDeniedFailedToLock"; 175 } 176 177 return nullptr; 178 } 179 180 /* static */ 181 void PointerLockManager::RequestLock(Element* aElement, 182 CallerType aCallerType) { 183 NS_ASSERTION(aElement, 184 "Must pass non-null element to PointerLockManager::RequestLock"); 185 186 RefPtr<Document> doc = aElement->OwnerDoc(); 187 nsCOMPtr<Element> pointerLockedElement = GetLockedElement(); 188 MOZ_POINTERLOCK_LOG("Request lock on element 0x%p [document=0x%p]", aElement, 189 doc.get()); 190 191 if (aElement == pointerLockedElement) { 192 DispatchPointerLockChange(doc); 193 return; 194 } 195 196 if (const char* msg = GetPointerLockError(aElement, pointerLockedElement)) { 197 DispatchPointerLockError(doc, msg); 198 return; 199 } 200 201 bool userInputOrSystemCaller = 202 doc->HasValidTransientUserGestureActivation() || 203 aCallerType == CallerType::System; 204 nsCOMPtr<nsIRunnable> request = 205 new PointerLockRequest(aElement, userInputOrSystemCaller); 206 doc->Dispatch(request.forget()); 207 } 208 209 /* static */ 210 void PointerLockManager::Unlock(const char* aReason, Document* aDoc) { 211 if (sLockedRemoteTarget) { 212 MOZ_ASSERT(XRE_IsParentProcess()); 213 MOZ_ASSERT(!sIsLocked); 214 MOZ_POINTERLOCK_LOG( 215 "Unlock document 0x%p [sLockedRemoteTarget=0x%p, reason=%s]", aDoc, 216 sLockedRemoteTarget, aReason); 217 218 if (aDoc) { 219 CanonicalBrowsingContext* lockedBc = 220 sLockedRemoteTarget->GetBrowsingContext(); 221 if (lockedBc && 222 lockedBc->TopCrossChromeBoundary()->GetExtantDocument() != aDoc) { 223 return; 224 } 225 } 226 227 (void)sLockedRemoteTarget->SendReleasePointerLock(); 228 sLockedRemoteTarget = nullptr; 229 return; 230 } 231 232 if (!sIsLocked) { 233 return; 234 } 235 236 nsCOMPtr<Document> pointerLockedDoc = GetLockedDocument(); 237 MOZ_POINTERLOCK_LOG("Unlock document 0x%p [LockedDocument=0x%p, reason=%s]", 238 aDoc, pointerLockedDoc.get(), aReason); 239 240 if (!pointerLockedDoc || (aDoc && aDoc != pointerLockedDoc)) { 241 return; 242 } 243 if (!SetPointerLock(nullptr, pointerLockedDoc, StyleCursorKind::Auto)) { 244 return; 245 } 246 247 nsCOMPtr<Element> pointerLockedElement = GetLockedElement(); 248 ChangePointerLockedElement(nullptr, pointerLockedDoc, pointerLockedElement); 249 250 if (BrowserChild* browserChild = 251 BrowserChild::GetFrom(pointerLockedDoc->GetDocShell())) { 252 browserChild->SendReleasePointerLock(); 253 } 254 255 AsyncEventDispatcher::RunDOMEventWhenSafe( 256 *pointerLockedElement, u"MozDOMPointerLock:Exited"_ns, CanBubble::eYes, 257 ChromeOnlyDispatch::eYes); 258 } 259 260 /* static */ 261 void PointerLockManager::ChangePointerLockedElement( 262 Element* aElement, Document* aDocument, Element* aPointerLockedElement) { 263 // aDocument here is not really necessary, as it is the uncomposed 264 // document of both aElement and aPointerLockedElement as far as one 265 // is not nullptr, and they wouldn't both be nullptr in any case. 266 // But since the caller of this function should have known what the 267 // document is, we just don't try to figure out what it should be. 268 MOZ_ASSERT(aDocument); 269 MOZ_ASSERT(aElement != aPointerLockedElement); 270 MOZ_POINTERLOCK_LOG("Change locked element from 0x%p to 0x%p [document=0x%p]", 271 aPointerLockedElement, aElement, aDocument); 272 if (aPointerLockedElement) { 273 MOZ_ASSERT(aPointerLockedElement->GetComposedDoc() == aDocument); 274 aPointerLockedElement->ClearPointerLock(); 275 } 276 if (aElement) { 277 MOZ_ASSERT(aElement->GetComposedDoc() == aDocument); 278 aElement->SetPointerLock(); 279 sLockedElement = do_GetWeakReference(aElement); 280 sLockedDoc = do_GetWeakReference(aDocument); 281 NS_ASSERTION(sLockedElement && sLockedDoc, 282 "aElement and this should support weak references!"); 283 } else { 284 sLockedElement = nullptr; 285 sLockedDoc = nullptr; 286 } 287 // Retarget all events to aElement via capture or 288 // stop retargeting if aElement is nullptr. 289 PresShell::SetCapturingContent(aElement, CaptureFlags::PointerLock); 290 DispatchPointerLockChange(aDocument); 291 } 292 293 /* static */ 294 bool PointerLockManager::StartSetPointerLock(Element* aElement, 295 Document* aDocument) { 296 if (!SetPointerLock(aElement, aDocument, StyleCursorKind::None)) { 297 DispatchPointerLockError(aDocument, "PointerLockDeniedFailedToLock"); 298 return false; 299 } 300 301 ChangePointerLockedElement(aElement, aDocument, nullptr); 302 nsContentUtils::DispatchEventOnlyToChrome( 303 aDocument, aElement, u"MozDOMPointerLock:Entered"_ns, CanBubble::eYes, 304 Cancelable::eNo, /* DefaultAction */ nullptr); 305 306 return true; 307 } 308 309 /* static */ 310 bool PointerLockManager::SetPointerLock(Element* aElement, Document* aDocument, 311 StyleCursorKind aCursorStyle) { 312 MOZ_ASSERT(!aElement || aElement->OwnerDoc() == aDocument, 313 "We should be either unlocking pointer (aElement is nullptr), " 314 "or locking pointer to an element in this document"); 315 #ifdef DEBUG 316 if (!aElement) { 317 nsCOMPtr<Document> pointerLockedDoc = GetLockedDocument(); 318 MOZ_ASSERT(pointerLockedDoc == aDocument); 319 } 320 #endif 321 322 PresShell* presShell = aDocument->GetPresShell(); 323 if (!presShell) { 324 NS_WARNING("SetPointerLock(): No PresShell"); 325 if (!aElement) { 326 sIsLocked = false; 327 // If we are unlocking pointer lock, but for some reason the doc 328 // has already detached from the presshell, just ask the event 329 // state manager to release the pointer. 330 EventStateManager::SetPointerLock(nullptr, nullptr); 331 return true; 332 } 333 return false; 334 } 335 RefPtr<nsPresContext> presContext = presShell->GetPresContext(); 336 if (!presContext) { 337 NS_WARNING("SetPointerLock(): Unable to get PresContext"); 338 return false; 339 } 340 341 nsCOMPtr<nsIWidget> widget; 342 nsIFrame* rootFrame = presShell->GetRootFrame(); 343 if (!NS_WARN_IF(!rootFrame)) { 344 widget = rootFrame->GetNearestWidget(); 345 NS_WARNING_ASSERTION(widget, 346 "SetPointerLock(): Unable to find widget in " 347 "presShell->GetRootFrame()->GetNearestWidget();"); 348 } 349 350 if (aElement && !widget) { 351 NS_WARNING("SetPointerLock(): No Widget while requesting pointer lock"); 352 return false; 353 } 354 355 sIsLocked = !!aElement; 356 357 // Hide the cursor and set pointer lock for future mouse events 358 RefPtr<EventStateManager> esm = presContext->EventStateManager(); 359 esm->SetCursor(aCursorStyle, nullptr, {}, Nothing(), widget, true); 360 EventStateManager::SetPointerLock(widget, presContext); 361 362 return true; 363 } 364 365 /* static */ 366 bool PointerLockManager::IsInLockContext(BrowsingContext* aContext) { 367 if (!aContext) { 368 return false; 369 } 370 371 nsCOMPtr<Document> pointerLockedDoc = GetLockedDocument(); 372 if (!pointerLockedDoc || !pointerLockedDoc->GetBrowsingContext()) { 373 return false; 374 } 375 376 BrowsingContext* lockTop = pointerLockedDoc->GetBrowsingContext()->Top(); 377 BrowsingContext* top = aContext->Top(); 378 379 return top == lockTop; 380 } 381 382 /* static */ 383 void PointerLockManager::SetLockedRemoteTarget(BrowserParent* aBrowserParent, 384 nsACString& aError) { 385 MOZ_ASSERT(XRE_IsParentProcess()); 386 if (sLockedRemoteTarget) { 387 if (sLockedRemoteTarget != aBrowserParent) { 388 aError = "PointerLockDeniedInUse"_ns; 389 } 390 return; 391 } 392 393 // Check if any popup is open. 394 if (IsPopupOpened()) { 395 aError = "PointerLockDeniedFailedToLock"_ns; 396 return; 397 } 398 399 MOZ_POINTERLOCK_LOG("Set locked remote target to 0x%p", aBrowserParent); 400 sLockedRemoteTarget = aBrowserParent; 401 PointerEventHandler::ReleaseAllPointerCaptureRemoteTarget(); 402 } 403 404 /* static */ 405 void PointerLockManager::ReleaseLockedRemoteTarget( 406 BrowserParent* aBrowserParent) { 407 MOZ_ASSERT(XRE_IsParentProcess()); 408 if (sLockedRemoteTarget == aBrowserParent) { 409 MOZ_POINTERLOCK_LOG("Release locked remote target 0x%p", 410 sLockedRemoteTarget); 411 sLockedRemoteTarget = nullptr; 412 } 413 } 414 415 PointerLockManager::PointerLockRequest::PointerLockRequest( 416 Element* aElement, bool aUserInputOrChromeCaller) 417 : mozilla::Runnable("PointerLockRequest"), 418 mElement(do_GetWeakReference(aElement)), 419 mDocument(do_GetWeakReference(aElement->OwnerDoc())), 420 mUserInputOrChromeCaller(aUserInputOrChromeCaller) {} 421 422 NS_IMETHODIMP 423 PointerLockManager::PointerLockRequest::Run() { 424 nsCOMPtr<Element> element = do_QueryReferent(mElement); 425 nsCOMPtr<Document> document = do_QueryReferent(mDocument); 426 427 const char* error = nullptr; 428 if (!element || !document || !element->GetComposedDoc()) { 429 error = "PointerLockDeniedNotInDocument"; 430 } else if (element->GetComposedDoc() != document) { 431 error = "PointerLockDeniedMovedDocument"; 432 } 433 if (!error) { 434 nsCOMPtr<Element> pointerLockedElement = do_QueryReferent(sLockedElement); 435 if (element == pointerLockedElement) { 436 DispatchPointerLockChange(document); 437 return NS_OK; 438 } 439 // Note, we must bypass focus change, so pass true as the last parameter! 440 error = GetPointerLockError(element, pointerLockedElement, true); 441 // Another element in the same document is requesting pointer lock, 442 // just grant it without user input check. 443 if (!error && pointerLockedElement) { 444 ChangePointerLockedElement(element, document, pointerLockedElement); 445 return NS_OK; 446 } 447 } 448 // If it is neither user input initiated, nor requested in fullscreen, 449 // it should be rejected. 450 if (!error && !mUserInputOrChromeCaller && !document->Fullscreen()) { 451 error = "PointerLockDeniedNotInputDriven"; 452 } 453 454 if (error) { 455 DispatchPointerLockError(document, error); 456 return NS_OK; 457 } 458 459 if (BrowserChild* browserChild = 460 BrowserChild::GetFrom(document->GetDocShell())) { 461 nsWeakPtr e = do_GetWeakReference(element); 462 nsWeakPtr doc = do_GetWeakReference(element->OwnerDoc()); 463 nsWeakPtr bc = do_GetWeakReference(browserChild); 464 browserChild->SendRequestPointerLock( 465 [e, doc, bc](const nsCString& aError) { 466 nsCOMPtr<Document> document = do_QueryReferent(doc); 467 if (!aError.IsEmpty()) { 468 DispatchPointerLockError(document, aError.get()); 469 return; 470 } 471 472 const char* error = nullptr; 473 auto autoCleanup = MakeScopeExit([&] { 474 if (error) { 475 DispatchPointerLockError(document, error); 476 // If we are failed to set pointer lock, notify parent to stop 477 // redirect mouse event to this process. 478 if (nsCOMPtr<nsIBrowserChild> browserChild = 479 do_QueryReferent(bc)) { 480 static_cast<BrowserChild*>(browserChild.get()) 481 ->SendReleasePointerLock(); 482 } 483 } 484 }); 485 486 nsCOMPtr<Element> element = do_QueryReferent(e); 487 if (!element || !document || !element->GetComposedDoc()) { 488 error = "PointerLockDeniedNotInDocument"; 489 return; 490 } 491 492 if (element->GetComposedDoc() != document) { 493 error = "PointerLockDeniedMovedDocument"; 494 return; 495 } 496 497 nsCOMPtr<Element> pointerLockedElement = GetLockedElement(); 498 error = GetPointerLockError(element, pointerLockedElement, true); 499 if (error) { 500 return; 501 } 502 503 if (!StartSetPointerLock(element, document)) { 504 error = "PointerLockDeniedFailedToLock"; 505 return; 506 } 507 }, 508 [doc](mozilla::ipc::ResponseRejectReason) { 509 // IPC layer error 510 nsCOMPtr<Document> document = do_QueryReferent(doc); 511 if (!document) { 512 return; 513 } 514 515 DispatchPointerLockError(document, "PointerLockDeniedFailedToLock"); 516 }); 517 } else { 518 StartSetPointerLock(element, document); 519 } 520 521 return NS_OK; 522 } 523 524 } // namespace mozilla